Java学习网络编程之在线聊天CS
距大二上学期学Java差不多快一年了,其实学习Java的有效时间也最多只有四五个月的样子(软工的课程多的无力吐槽,很摒弃这种多而不精的教学方式),说实话真的很喜欢Java,特别是用它编出游戏时特别有成就感,由其记得大二这两个学期期末考试复习阶段没怎么认真复习,而是在自习室“偷偷地”编Java程序(也是作为一种放松的方式吧)。自己写的、跟着书本上的程序写的Java代码也不少了,今天决定这个暑期把它们以博客的形式整理出来,一是为了温故而知新,二是希望高手大牛们能够给我指点一二。
首先整理出来的就是这个在线聊天CS了,记得它的雏形是《Java语言程序设计——进阶篇》里面网络那一章节的一个程序,其原本目的是做一个服务器和一个客户端,客户端向服务器发送一个圆的半径(double型),然后服务器通过计算出圆的面积并将这个值(double型)发送回客户端,我把书上的代码打下来之后就想这个原理完全可以做一个在线聊天CS,于是断断续续花了三四天自己慢慢摸索出来了这个程序。
以下是该程序的三大核心:
一、套接字编程
这部分我完全是看着书上的知识点来编码的。
其中套接字分为服务器套接字和客户端套接字:
ServerSocket serverSocket = new ServerSocket(port);
//创建一个服务器套接字,并把这个套接字附加在端口port上
Socket socket = serverSocket.accept();
//一直等待,直到一个客户端连接到服务器套接字
Socket socket = new Socket(serverName,port);
//打开一个套接字,使客户端程序能够和服务器程序通信,其中serverName是服务器的互联网主机名或者IP地址,当主机名为localhost或者127.0.0.1时表示为客户端正在运行的主机。
以上三个语句基本上就可以完成客户端程序和服务器程序之间的通信了,这学期正好学了《计算机网络》(准确地说是这学期最后三个星期),使我更加深入地理解了套接字的运行机制,引用书上的一句话“套接字是应用层和运输层的粘合剂”。
二、通过套接字进行数据传输
这一部分也是书上现成给出的类库。即DataInputStream和DataOutputStream。服务器和客户端通过这两个类来进行输入输出流的读写操作,书中通过DataInputStream里面的readDouble()方法来读取浮点型的半径、通过DataOutputStream里面的writeDouble(area)方法来写(发送)浮点型的面积。
但是聊天的时候传输的数据是字符串类型的,readDouble()和writeDouble(double)这两个方法显然不能满足,于是我尝试了DataInputStream类的readLine()方法,可是编译器居然在这段语句上划了一个横线表示不能用。。。。。。有点无语,网上一看原来是过期了,对我这个Java小学童也算是一件新鲜事儿吧。后来我又试了readChar等方法,虽然没有过期,但是传输过去的是乱码。。。。。。最后我还是决定用最原始的readByte方法(也是最后一个剩下的预期可行方法)来对一个一个的字节进行操作,然后将其转换为字符类型。
while(inputFromClient.available() != 0 ){
Byte c = inputFromClient.readByte();
if(c < 0){
heSaid += (char)(c + 256);
}
else{
heSaid += (char)(c + 0);
}
}
上面就是我最终的解决办法,其中的转换方式是查找网上大神提出的方法。其中inputFromClient对象相当于数据结构里面的队列,当该队列不为空的时候就从该队列的队首取出一个Byte,然后根据转换规则将其转换为字符类型的数据并且加入到heSaid这个字符串中,读取的同时肯定也进行了pop()操作,即将队首的数据元素弹出队列,这样操作直到该队列为空,也就完成了整个读取以及存储转换的过程。
同样,数据的输出也是通过字节的形式输出,即调用了DataOutputStream里面的writeBytes()方法。
三、线程机制实现服务器程序和客户端程序的交互与同步
由于书中只设置了一个服务器线程,并且这个线程一直在运行,所以很难实现两个程序之间的交互,一开始由于没有认识到这个问题导致做出来的CS只能必须先由客户端发送消息才能通信,而且经常出现“抢占现象”,即发出的消息只有一部分显示在对方的文本框内,另外一部分丢失或者出现在其他位置,这个问题纠结了不少时间,后来我突然想到这学期《计算机操作系统》里面的生产者和消费者进程,当时我在编写这个同步程序的时候也遇到了类似的情况,于是我果断设置了客户端和服务器两个线程,并且让这两个线程“自动的”进行P、V操作,即sleep(100),这样就较好的解决了进程之间的同步问题,当然也可以使用信号量机制来实现。
以上三点算是我觉得比较核心的部分吧,掌握了以上三点基本上就可以完成这个CS的编写了。
以下是这个程序运行的截图:
服务器类和客户端类的代码如下:
Server:
package com.web.demo;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class Server extends JFrame{
private JTextField jtfInput = new JTextField();
private JTextArea jta = new JTextArea();
private int flagForYou = 0;//表示按下回车键
private int flagForHim = 0;//表示对方的输入
private String youSaid = "";
private String heSaid = "";
private ServerSocket serverSocket;
private Socket socket;
private DataInputStream inputFromClient;
private DataOutputStream outputToClient;
public static void main(String[] args) {
new Server();
}
public Server(){
TextFieldListener l = new TextFieldListener();
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(new JLabel("Enter Information"),BorderLayout.WEST);
p.add(jtfInput,BorderLayout.CENTER);
jtfInput.setHorizontalAlignment(JTextField.RIGHT);
jtfInput.addActionListener(l);
this.setLayout(new BorderLayout());
add(p,BorderLayout.NORTH);
this.setTitle("Server");
this.setSize(500,500);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jta.setEditable(false);
this.add(new JScrollPane(jta),BorderLayout.CENTER);
this.setVisible(true);
try {
serverSocket = new ServerSocket(8000);
//创建一个服务器,需要创建一个服务器套接字,并把它附加在一个端口上,服务器通过这个端口监听连接
jta.append("Server Start at " + new Date() + '\n');
socket = serverSocket.accept();
//该语句会一直等待,直到一个客户端连接到服务器的套接字上
inputFromClient = new DataInputStream(socket.getInputStream());
//服务器从客户端接收数据
outputToClient = new DataOutputStream(socket.getOutputStream());
//服务器向客户端发送数据
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
start();
}
private class ServerThread implements Runnable{
public void run() {
while(true){
try {
heSaid = "";//将上一次的话清空
if(inputFromClient.available() != 0 ){
flagForHim = 1;
}
while(inputFromClient.available() != 0 ){
Byte c = inputFromClient.readByte();
if(c < 0){
heSaid += (char)(c + 256);
}
else{
heSaid += (char)(c + 0);
}
}
if(youSaid != "" && flagForYou == 1){//按下回车键并且文本框内的字符串不为空
outputToClient.writeBytes(youSaid);
jta.append("You said :" + '\n' + " " + youSaid + '\n');
flagForYou = 0;
}
if(heSaid != "" && flagForHim == 1){
jta.append("He said :" + '\n' + " " + heSaid + '\n');
flagForHim = 0;
}
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void start(){
new Thread(new ServerThread()).start();
}
private class TextFieldListener implements ActionListener{
public void actionPerformed(ActionEvent e) {
if(jtfInput.getText().length() != 0){
youSaid = jtfInput.getText();
flagForYou = 1;
jtfInput.setText("");
}
}
}
}
Client:
package com.web.demo;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class Client extends JFrame{
private JTextField jtfInput = new JTextField();
private JTextArea jta = new JTextArea();
private DataOutputStream toServer;
private DataInputStream fromServer;
private int flagForYou = 0;
private int flagForHim = 0;
private String youSaid = "";
private String heSaid = "";
public static void main(String[] args){
new Client();
}
public Client(){
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(new JLabel("Enter Information"),BorderLayout.WEST);
p.add(jtfInput,BorderLayout.CENTER);
jtfInput.setHorizontalAlignment(JTextField.RIGHT);
jta.setEditable(false);
this.setLayout(new BorderLayout());
add(p,BorderLayout.NORTH);
add(new JScrollPane(jta),BorderLayout.CENTER);
jtfInput.addActionListener(new TextFieldListener());
this.setTitle("Client");
this.setSize(500,500);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
try {
Socket socket = new Socket("localhost",8000);
//客户端执行该语句,请求与服务器连接,前者为服务器的互联网主机名或IP地址
fromServer = new DataInputStream(socket.getInputStream());
toServer = new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
jta.append(e.toString() + '\n');
//e.printStackTrace();
}
start();
}
private class ClientThread implements Runnable{
public void run(){
while(true){
try {
heSaid = "";
if(fromServer.available() != 0){
flagForHim = 1;
}
while(fromServer.available() != 0){
Byte c = fromServer.readByte();
if(c < 0){
heSaid += (char)(c + 256);
}
else{
heSaid += (char)(c + 0);
}
}
if(youSaid != "" && flagForYou == 1){
toServer.writeBytes(youSaid);
jta.append("You said :" + '\n' + " " + youSaid + '\n');
flagForYou = 0;
}
if(heSaid != "" && flagForHim == 1){
jta.append("He said :" + '\n' + " " + heSaid + '\n');
flagForHim = 0;
}
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void start(){
new Thread(new ClientThread()).start();
}
private class TextFieldListener implements ActionListener{
public void actionPerformed(ActionEvent e){
if(jtfInput.getText().length() != 0){
youSaid = jtfInput.getText();
flagForYou = 1;
jtfInput.setText("");
}
}
}
}