小小的java socket聊天程序

      本程序是基于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只是个单线程程序,他实现了多线程才能实现的功能。
   
总结:只有经过亲自动手实践实现预想的功能,才能让自己成长更快。结果虽小,但享受了逐步思考的过程。
如有转载,请说明出处!
java聊天程序源码 2 需求分析 2.1 业务需求 1. 与聊天室成员一起聊天。 2. 可以与聊天室成员私聊。 3. 可以改变聊天内容风格。 4. 用户注册(含头像)、登录。 5. 服务器监控聊天内容。 6. 服务器过滤非法内容。 7. 服务器发送通知。 8. 服务器踢人。 9. 保存服务器日志。 10.保存用户聊天信息。 2.2 系统功能模块 2.2.1 服务器端 1.处理用户注册 2.处理用户登录 3.处理用户发送信息 4.处理用户得到信息 5.处理用户退出 2.2.2 客户端 1.用户注册界面及结果 2.用户登录界面及结果 3.用户发送信息界面及结果 4.用户得到信息界面及结果 5.用户退出界面及结果 2.3 性能需求 运行环境:Windows 9x、2000、xp、2003,Linux 必要环境:JDK 1.5 以上 硬件环境:CPU 400MHz以上,内存64MB以上 3.1.2 客户端结构 ChatClient.java 为客户端程序启动类,负责客户端的启动和退出。 Login.java 为客户端程序登录界面,负责用户帐号信息的验证与反馈。 Register.java 为客户端程序注册界面,负责用户帐号信息的注册验证与反馈。 ChatRoom.java 为客户端程序聊天室主界面,负责接收、发送聊天内容与服务器端的Connection.java 亲密合作。 Windowclose 为ChatRoom.java的内部类,负责监听聊天室界面的操作,当用户退出时返回给服务器信息。 Clock.java 为客户端程序的一个小程序,实现的一个石英钟功能。 3. 2 系统实现原理 当用户聊天时,将当前用户名、聊天对象、聊天内容、聊天语气和是否私聊进行封装,然后与服务器建立Socket连接,再用对象输出流包装Socket的输出流将聊天信息对象发送给服务器端 当用户发送聊天信息时,服务端将会收到客户端用Socket传输过来的聊天信息对象,然后将其强制转换为Chat对象,并将本次用户的聊天信息对象添加到聊天对象集Message中,以供所有聊天用户访问。 接收用户的聊天信息是由多线程技术实现的,因为客户端必须时时关注更新服务器上是否有最新消息,在本程序中设定的是3秒刷新服务器一次,如果间隔时间太短将会增加客户端与服务器端的通信负担,而间隔时间长就会让人感觉没有时效性,所以经过权衡后认为3秒最佳,因为每个用户都不可能在3秒内连续发送信息。 当每次用户接收到聊天信息后将会开始分析聊天信息然后将适合自己的信息人性化地显示在聊天信息界面上。 4.1.1 问题陈述 1.接受用户注册信息并保存在一个基于文件的对象型数据库。 2.能够允许注册过的用户登陆聊天界面并可以聊天。 3.能够接受私聊信息并发送给特定的用户。 4.服务器运行在自定义的端口上#1001。 5.服务器监控用户列表和用户聊天信息(私聊除外)。 6.服务器踢人,发送通知。 7.服务器保存日志。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值