多人聊天室的实现

多人聊天室

一、功能简介

每个客户端在连接到服务器端时,开始发送消息到服务端,服务端在接收到客户端的连接时,首先输出谁进入了聊天室,然后把客户端发来的消息转发给其他客户端,实现群聊的功能,最终达到实现多功能(快速、实时、多人)的多人聊天给用户带来更好的体验功能。

二、设计构想

  • 设计客户端及服务器的界面

  1. 输入框与输出框
  2. 用户昵称
  3. 消息时间
  4. 添加滚动条
  5. 按钮的添加
  6. 功能键的设置
  7. 用户进出的显示
  • 服务器端与客户端的交互

  1. 首先服务器端要实例化一个ServerSocket对象,并用其中的accept()方法等待客户端的连接。
    这里值得注意的是:ServreSocket类中的accept()方法是一个阻塞方法,也就是说accept()方法在获取客户端的连接之前会一直进行阻塞式的等待,不会让其下面的代码执行,直到得到客户端的连接为止。

  2. 服务器一直等待的同时,我们就要建立客户端并向服务器申请连接。那么在客户端代码中,我们要实例化一个Socket对象,其应该具有和服务器端ServreSocket相同的端口号,以此确保对服务器提出准确的连接。在成功实例化创建Socket对象后,就相当于成功和服务器建立了连接(前提是申请的服务器对象已创建并在执行accept()方法等待)。

  3. 在客户端成功提出连接申请后,我们再回到服务器端accept()方法上来,accept()方法在成功得到连接申请后,返回的值是一个Socket对象,该Socket对象是通向该客户端的连接。
  4. 在成功通过Socket对象和ServerSocket对象在客户端与服务器间建立TCP连接后,我们就要开始进行服务器与客户端间的信息交流了,其手段则是通过IO流的实现: 通过Socket类中的getInputStream()方法和getOutputStrea()方法可以获取对应方向的输入流和输出流。

  5. 服务器端通过对accept()得到的Socke对象使用getOutputStrea()方法创建的OutputStream流可以向客户端写入信息,同理对该Socket对象使用getInputStream()方法获取的InputStream流可以从客户端中读取信息。反之客户端亦然。
  • 优化界面

  1. 解决后续BUG
  2. 将用户界面更加简化不繁琐

三、实验结果展示

  • 服务器端口

  •  客户端端口

  • 消息的交互

四、具体代码及步骤

  • 界面设计

1.客户端

    public void init(){
        
        this.setTitle("客户端窗口");
        this.add(sp,BorderLayout.CENTER);
        this.add(tf,BorderLayout.SOUTH);
        this.setBounds(300,300,300,400);        //给输入框设置监听
        tf.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String strSend = tf.getText();   //获取输入框中的文本
                if(strSend.trim().length()==0){  //若输入框为空则不进行操作(检查)
                    return;
                }
                //strSend发送服务器的方法
                send(strSend);   // 将文本内容送到发送服务器方法中
                tf.setText("");  // 文本在输入完后消失在文本框
            } 
        });
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  //关闭窗口及关闭程序
        ta.setEditable(false); //不能在显示框中打字
        tf.requestFocus();     //光标聚焦
        try {
            s= new Socket(CONNSTR,CONNPORT);
            //表示连上服务器
            isConn = true;
            
        } catch (UnknownHostException e1) {
            // TODO 自动生成的 catch 块
            e1.printStackTrace();
        } catch (IOException e1) {
            // TODO 自动生成的 catch 块
            e1.printStackTrace();
        }
        this.setVisible(true);  //将构造好的数据模型显示出来
        
        new Thread(new Receive()).start();//启动多线程

2.服务器端

public ServerChat(){
        this.setTitle("服务器端");
        this.add(sp,BorderLayout.CENTER); 
        btnTool.add(startBtn);
        btnTool.add(stopBtn);
        this.add(btnTool,BorderLayout.SOUTH); 
        this.setBounds(0,0,500,500);
        if(isStart){  //判断服务器是否启动
            serverta.append("服务器已启动\n");
        }else{
            
            serverta.append("服务器还未启动,请点击按钮启动\n");
            
        }
        
        //给窗口关闭键赋予监听
        
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                isStart = false ;
                try {
                    
                    if (ss!=null){
                        ss.close();  
                    }
                    System.out.println("服务器停止!"); 
                    serverta.append("服务器断开");
                    System.exit(0);  //退出程序  下面关闭按钮同理
                } catch (IOException e1) {
                    // TODO 自动生成的 catch 块
                    e1.printStackTrace();
                }
            }
        });
        //给关闭按纽加监听
        stopBtn.addActionListener(new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent e) {
                
                try {
                    
                    if (ss!=null){
                        ss.close();   //关闭服务器连接端口
                        isStart = false;
                    }
                    System.exit(0);
                    serverta.append("服务器断开");
                    System.out.println("服务器停止!"); 
                } catch (IOException e1) {
                    // TODO 自动生成的 catch 块
                    e1.printStackTrace();
                }
            }
        });
        //给启动按钮设置一个监听
        startBtn.addActionListener(new ActionListener() { 
            
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("成功启动");
                try {
                    
                    if(ss == null){
                        ss= new ServerSocket(PORT);  //创建一个服务器端口号
                        
                    }
                    isStart = true;  //启动条件变为真
                    serverta.append("服务器已经启动了!"+"\n"); 
                        
                } catch (IOException e1) {
                    // TODO 自动生成的 catch 块
                    e1.printStackTrace();
                }
                
            }
        });
        
    
        
        this.setVisible(true);  //将构造好的数据模型显示出来
        startServer(); 
        
    }

 

  • 客户端与服务器端口的交互

1.在用户端建立与服务器端的通道

dos = new DataOutputStream(s.getOutputStream()); //建立一根发送管道

 

2.在服务器上为客户端创建端口号

try{
            try{
                ss = new ServerSocket(PORT);  //创建端口号
                isStart=true;
            }catch (IOException e2){
                e2.printStackTrace();
            }
            //可以接收多个客户端的连接
            while(isStart){                     //在这使用是否启动来作为循环判断条件
                Socket s =ss.accept();          //在这等着接客户端的信息 一个客户端连一个服务器接口
                ccList.add(new ClientConn(s));  //每来一个将信息加入到集合中
                System.out.println("一个客户端连接服务器:"+s.getInetAddress()+"/"+s.getPort());
                serverta.append("一个客户端连接服务器:"+s.getInetAddress()+"/"+s.getPort()+"\n");
            }

 

3.客户端发送消息

    public void send(String str){  //得到要发送的文本
        try {
            dos = new DataOutputStream(s.getOutputStream()); //建立一根发送管道
            dos.writeUTF(str);
        } catch (IOException e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }

 

4.服务器端接收消息

public void run() {
            
            try {
                DataInputStream dis =new DataInputStream(s.getInputStream());
                //为了能让服务器收到每个客户端多句话
                while(isStart){  //在这使用是否启动来作为循环判断条件
                    String str =dis.readUTF(); //接收文本
                    System.out.println(s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n");//显示在控制台上
                    serverta.append(s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n");   //显示在服务器端
                    String strSend = s.getLocalAddress()+"|"+s.getPort()+"说:"+str+"\n";
                    //遍历 ccList 调用send方法 在客户端接收信息是多线程的接收信息(多线程的发送消息)                    
                    java.util.Iterator<ClientConn> it = ccList.iterator();
                    while(it.hasNext()){
                        ClientConn o = it.next();
                        o.send(strSend);      
                    }
                    
                }
                
            } catch (SocketException e){
                System.out.println("一个客户端下线了\n");
                serverta.append(s.getLocalAddress()+"|"+s.getPort()+"客户端下线了\n");  //建立发送的管道
                
            }catch (IOException e) {
                // TODO 自动生成的 catch 块
                e.printStackTrace();
            }            
        }

 

5.服务器发送消息

        public void send(String str){
            try {
                DataOutputStream dos = new DataOutputStream(this.s.getOutputStream());
                dos.writeUTF(str);
                
                
            } catch (IOException e) {
                // TODO 自动生成的 catch 块
                e.printStackTrace();
            }
        }

 

6.客户端接收消息

    class Receive implements Runnable{ 
        @Override
        public void run() {
            try {
                while(isConn){
                    DataInputStream dis = new DataInputStream(s.getInputStream());
                    String str  = dis.readUTF();
                    ta.append(str);
                    
                    
                }
            }  catch (SocketException e) {
                
                System.out.println("服务器意外终止!");
                
                ta.append("服务器意外终止!\n");
            }catch (IOException e) {
                // TODO 自动生成的 catch 块
                e.printStackTrace();
            }           
        }
    }

 

7.遍历客户端发来的消息(为了防止消息发送回自身)

java.util.Iterator<ClientConn> it = ccList.iterator();
                    while(it.hasNext()){
                        ClientConn o = it.next();
                        o.send(strSend);      
                    }

五、总结

  • 经过查阅大料博客以及相关文献来完成多人聊天室项目,虽然我们的代码能力提高的并不是很明显,但是经过此役之后,我的java功底加深了,设计以及对项目的思考有了一定的广度和深度,还有就是在查阅相关博客的同时也就是在取之精华进行总结归纳
  • 做完多人聊天项目之后,对我最大的帮助就是,让我了解到一个中间层次 Socket的概念
  • 我们原一直以为,客户端发送消息之后服务端必须立刻马上进行处理,但其实还可以设置一些中间转发的过程,比如消息队列用于保存发送过程中的数据,这样做可提高系统的响应速率及系统稳定性,从而避免了一些不确定因素,比如(消息高峰时的解耦和),如果发送方和接收方步频不一致,中间转发可以达到弥补生产者消费者步频的不一致问题

  • 在编写服务端代码的过程中,处处有坑,报出的错误要一一的进行处理以达到更好的优化

 

 

小组成员:

刘龙军、郭润方、惠文凯、邢润虎

 

链接:https://pan.baidu.com/s/1kP3PY71-Ev-e_ulc7MNomg
提取码:4ezx

 

posted @ 2022-01-04 13:04  szmtjs10  阅读(1172)  评论(0编辑  收藏  举报