zoukankan      html  css  js  c++  java
  • 多人聊天室的实现

    多人聊天室

    一、功能简介

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

    二、设计构想

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

    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

  • 相关阅读:
    洛谷P1908《逆序对》
    洛谷P3884《[JLOI2009]二叉树问题》
    最近公共祖先 LCA
    洛谷P1531《I Hate It》
    洛谷P1563「NOIP2016」《玩具谜题》
    乘法逆元求法
    CF56E 【Domino Principle】
    CF638C 【Road Improvement】
    Luogu
    2018.8.7提高B组模拟考试
  • 原文地址:https://www.cnblogs.com/szmtjs10/p/15710938.html
Copyright © 2011-2022 走看看