小小的java socket聊天程序

本文介绍了一个基于Java Socket的聊天程序设计过程,包括单线程与多线程的实现方式。从简单的控制台应用程序到具备GUI界面的聊天程序,详细探讨了如何解决阻塞问题,并最终实现了友好用户交互。
      本程序是基于java语言的Socket聊天程序,采用TCP传输协议,实现两个人之间的信息交互。在形成最终结果之前,我经历了两个过程程序,两个过程均为半成品,他们反映了我整个课程设计中的思考过程,在一个较为系统思考过程后,socket思想一层一层加固,让我映像很深刻,收获很多。在此文档中,我将演示我两个过程程序的思考过程,然后对源代码进行讲解,但最终上传的代码为制成品。
      在两个过程程序中,TestServer1和TestClient1为第一个过程,TestServer2和TestClient2为第二个过程,MySingleThreadServer1和MySingleThreadClient1为最终程序。其中,TestServer2和TestClient2实现了多线程,一个线程负责接收,另一个线程负责发送,MySingleThreadServer1和MySingleThreadClient1实现了基于GUI的聊天。控件并非像MFC那样很容易就可以通过拖动组件实现,java是通过程序的编写来实现,我采用awt组件编码实现GUI,界面很简单,但花了不少时间来布局。
 
核心代码讲解:
      三个过程程序都牵涉了java socket编程最核心的思想,以下为核心的讲解。
1.服务器端
      Java Socket服务器端需要引入两个包,java.io包和java.net包,io包是解决输入输出流的问题,而net包包含了socket编程所需的API.
      服务器端首先要得到ServerSocket的对象,即ServerSocket ss = new ServerSocket(5555); 5555为服务器端的端口号。Socket s = ss.accept();服务器端Socket对象通过accept()方法开始监听链接过来的客户端信息。如果有客户端有信息过来,则对象s调用输入输出流的方法,如s.getInputStream(),同时把得到的InputStream 封装在DataInputStream当中,在客户端与服务器端通信时,有可能两端存在于不同的操作系统,封装在DataInputStream可以很好的解决这个问题。
 
2.客户端
      其实客户端代码与服务器端大多类似,有一点显著不同,在客户端没有ServerSocket类,即客户端不用监听任何链接,他只需要发送链接即可。Socket s = new Socket(String IPAddr,int port),IPAddr为服务器端的IP地址,port为服务器端的端口号即5555,由于本程序服务器端和客户端在同一主机上,所以服务器端IP地址为127.0.0.1。通过IPAddr和port两个参数就可以得到Socket对象s,接下来的步骤就和服务器程序类似了。
   
                                                        对三个过程程序的详解
 
Procedure1:
    服务器端核心代码如下:
               ServerSocket ss = new ServerSocket(5555);
               Socket s = ss.accept();
                    
               OutputStream os = s.getOutputStream();
               DataOutputStream dos = new DataOutputStream(os);
              
               InputStream is = s.getInputStream();
               DataInputStream dis = new DataInputStream(is);
              
               InputStreamReader isr = new InputStreamReader(System.in); //重键盘读入数据
               BufferedReader br = new BufferedReader(isr);  //把从键盘读入的数据放入缓冲
              
               String info;
               while(true){
                      info = dis.readUTF();                   
                      System.out.println("客户端说:" + info);
                      if(info.equals("goodbye")){
                             break;
                      }
                      info = br.readLine();
                      dos.writeUTF(info);
                      System.out.println("服务器说:" + info);
                      if(info.equals("goodbye")){
                             break;
                      }
              }
             
 
 客户端核心代码如下:
                     Socket s = new Socket("127.0.0.1",5555);
                    
                     InputStream is = s.getInputStream();
                     DataInputStream dis = new DataInputStream(is);
                    
                     OutputStream os = s.getOutputStream();
                     DataOutputStream dos = new DataOutputStream(os);
                    
                     InputStreamReader isr = new InputStreamReader(System.in);
                     BufferedReader br = new BufferedReader(isr);
                    
                     String info;
                     while(true){
                            info = br.readLine();
                            System.out.println("客户端说的是:" + info);
                            dos.writeUTF(info);
                            if(info.equals("goodbye")){
                                   break;
                            }
                            info = dis.readUTF(); //阻塞函数
                            System.out.println("服务器端说的是:" + info);
                            if(info.equals("goodbye")){
                                   break;
                            }
                           
                     }
 
      服务器端和客户端在while(true)循环处各不相同,服务器端是dis.readUTF(),必须首先读取客户端传过来的信息,才能通过info = br.readLine();dos.writeUTF(info);从键盘中读取信息再发送给客户端。相反,客户端必须首先通过br.readLine();读取键盘信息,才能接收服务器发送来的信息。
 
      思考1:这个简易的聊天程序已经实现了服务器和客户端的信息交互,但此时已经出现了一个必然出现的问题,比如拿服务器端来讲,当服务器通过dos.writeUTF(info)发送消息给客户端后,在while循环体内,他又要执行info = dis.readUTF()代码,而readUTF()是一个阻塞函数,如果客户端没有发送过来,他就阻塞在那个地方,此时下面部分的代码dos.writeUTF(info)就不能执行,即服务器端不能发送消息出去。
      要怎样解决这个问题呢?怎样readUTF()阻塞的同时又可以writeUTF(info)发送出消息呢?显然,一条路径走不通时应该考虑走另一条路,于是,多线程在这里引入了。Procedure2就是这样出来的。
 
 
Procedure2:
      Procedure2相比procedure1加入了多线程的部分,一个线程负责专门去接受消息,另一个负责发送消息。当服务器端负责接收的线程因为readUTF()被阻塞不能发送消息时,负责发送消息的线程让服务器端也能发送消息。同样的原理,客户端也如此。
 
服务器端加入的关键代码如下:
class ServerReadThread extends Thread{
             
       private DataInputStream dis;
       public ServerReadThread(DataInputStream dis){
                     this.dis = dis;
       }
       public void run(){
              String info;
              try{
                    while(true){
                       info = dis.readUTF();
                       System.out.println("客户端说:" + info);
                       if(info.equals("goodbye")){
                             System.out.println("客户端拜拜了!");
                             System.exit(0);
                    }
              }
              }catch(IOException e){
                     e.printStackTrace();
              }
       }
}
 
class ServerWriteThread extends Thread{
      
       private DataOutputStream dos;
       private BufferedReader br;
       public ServerWriteThread(DataOutputStream dos,BufferedReader br){
              this.dos = dos;
              this.br = br;
       }
       public void run(){
              String info;
              try{
                    while(true){
                       info = br.readLine();
                       dos.writeUTF(info);
                       if(info.equals("bye")){
                              System.exit(0);
                    }
                }
              }catch(IOException e){
                     e.printStackTrace();
              }
       }
}
 
      负责接受的类ServerReadThread继承Thread,并构造一个DataInputStream对象参数的构造函数,接收对方信息。负责发送的类ServerWriteThread继承Thread,并构造一个DataInputStream对象和BufferedRead对象的双参数的构造函数,负责发送消息。new ServerReadThread(dis).start();new ServerWriteThread(dos,br).start();开启两个线程。
      客户端原理与服务器端类似,就不做更详细的讲解。
 
      思考2:procedure2相比procedure1已经实现了多线程聊天,虽只是运行在控制台上,但麻雀虽小,五脏俱全,这已经体现出了java Socket编程以及多线程的核心思想,此课程设计的目的已经达到。为实现更加人性化的效果,我引入javaGUI的组件awt,与之相关的技术也就随之引入了,比如基于事件的驱动,还有awt各个组件之间的调用。与此同时,代码的分布和组织结构都要做相应的调整。
 
 
Procedure3
      由于是基于事件驱动的组件,所以MySingleThreadServer1类要继承ActionListener接口,实现该接口唯一的方法actionPerformed(ActionEvent e),即当触发某一事件时,执行该方法内的代码。关键代码如下:
public class MySingleThreadServer1 implements ActionListener{
 
private Frame f;
private TextArea ta1 = newTextArea("",5,40,TextArea.SCROLLBARS_VERTICAL_ONLY);       
private TextArea ta2 = new TextArea("",16,52,TextArea.SCROLLBARS_VERTICAL_ONLY); 
private Button b;
private String msg ="";
OutputStream os;
DataOutputStream dos;
InputStream is;
DataInputStream dis;
ServerSocket ss;
Socket s;
    
      public MySingleThreadServer1(){
           f = new Frame("server:小马");
           b = new Button("服务器发送");
           f.setBackground(Color.WHITE);
           b.setBackground(Color.LIGHT_GRAY);
           ta1.setBackground(Color.LIGHT_GRAY);
           ta2.setBackground(Color.LIGHT_GRAY);
           ta2.setEditable(false);               //set to only be read
           f.setLayout(new FlowLayout(FlowLayout.LEFT));
           f.add(ta1);
              f.add(b);
              f.add(ta2);
             
              f.setLocation(200,200);
              f.setSize(400,400);
              f.setResizable(false);
              f.setVisible(true);
              b.addActionListener(this);       
              f.addWindowListener(new WindowAdapter(){
                     public void windowClosing(WindowEvent e){
                            System.exit(0);  
                     }
              });
              try{
                  ss = new ServerSocket(7777);
                  s = ss.accept();
                 
                  is = s.getInputStream();
                  dis = new DataInputStream(is);
                 
                  os = s.getOutputStream();
                  dos = new DataOutputStream(os);
                 
                  serverReadSome();     //接受客户端发来的信息
                 
           }catch(IOException e){
                  e.printStackTrace();
           }
      
    }
   
    public void actionPerformed(ActionEvent e){  //服务器点击按钮触发时间
          
           try{
               msg = ta1.getText();
               dos.writeUTF(msg);
               ta2.append("小马:"+msg+"/n");
               ta1.setText("");
               ta1.requestFocus();
           }catch(IOException ioe){
                  ioe.printStackTrace();
           }
    }
   
    public void  serverReadSome(){
           try{
             while(true){
               msg = dis.readUTF();
                  ta2.append("小徐:"+msg+"/n");
             }
           }catch(IOException ioe){
                  ioe.printStackTrace();
           }
    }
   
    public static void main(String args[]){
           new MySingleThreadServer1();
    }
}
 
      我把GUI的初始化信息和事件驱动的信息放到了MySingleThreadServer1的构造函数中,使之new一个的时候就初始化该类。
     
      思考3:由于DataInputStream,DataOutputStream, InputStream, OutputStream, ServerSocket,Socket的对象是全局变量,所以我没有把对这些类对象的操作即输入输出流放到main()函数中,应为在main()函数中必须要求是静态的变量或方法,于是,我干脆一道把对输入输出流的操作放到了构造函数里面。同时,在actionPerformed(ActionEvent e)方法体内实现了相应的逻辑处理。
      经过千辛万苦的代码调试,终于实现了两者之间的信息交互。由于是单线程,我本以为procedure3也会出现procedure1那样被readUTF()阻塞的状况,结果出乎意料,基于awt的小小聊天程序并没有出现预想中的糟糕现象,即一方发送后必须等待对方回应才能再次发送的现象,procedure3直接出现了procedure2中靠多线程才能实现的功能。奇怪,难道是老天开眼,知道我努力就放我一马。再把代码仔细分析,知道为什么了。
      原因是这样:actionPerformed()方法负责发送消息,当一点击按钮时就触发actionPerformed()发送消息,只要一方编辑消息并点击按钮,就会发送消息到对方,不会受readUTF()阻塞函数的影响。所以,严格意义上讲,Procedure3只是个单线程程序,他实现了多线程才能实现的功能。
   
总结:只有经过亲自动手实践实现预想的功能,才能让自己成长更快。结果虽小,但享受了逐步思考的过程。
如有转载,请说明出处!
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值