• 【第98题】JAVA高级技术-网络编程17(简易聊天室12:实现客户端一对一聊天)


    回城传送–》《JAVA筑基100例》

    零、前言

    ​ 今天是学习 JAVA语言 打卡的第98天,每天我会提供一篇文章供群成员阅读( 不需要订阅付钱 ),读完文章之后,按解题思路,自己再实现一遍。在小虚竹JAVA社区 中对应的 【打卡贴】打卡,今天的任务就算完成了。

    ​ 因为大家都在一起学习同一篇文章,所以有什么问题都可以在群里问,群里的小伙伴可以迅速地帮到你,一个人可以走得很快,一群人可以走得很远,有一起学习交流的战友,是多么幸运的事情。

    ​ 学完后,自己写篇学习报告的博客,可以发布到小虚竹JAVA社区 ,供学弟学妹们参考。

    ​ 我的学习策略很简单,题海策略+ 费曼学习法。如果能把这100题都认认真真自己实现一遍,那意味着 JAVA语言 已经筑基成功了。后面的进阶学习,可以继续跟着我,一起走向架构师之路。

    一、题目描述

    题目实现:不同的客户端之间需要进行通信,一个客户端与指定的另一客户端进行通信,实现一对一聊天功能。

    实现一个客户端与指定的另一客户端进行通信,运行程序,服务器启动后,启动3个客户端程序,分别以小小,虚虚,竹竹,登录 ,然后在左侧的用户列表中选择接收信息用户,输入聊天信息,发送到目标用户。

    二、解题思路

    创建一个服务类:ClientOneToOneServerFrame,继承JFrame类

    定义ServerThread线程类,用于为客户端添加用户列表。有一部分代码用于转发客户端发送的消息。

    创建一个客户端类:ClientOneToOneClientFrame,继承JFrame类

    定义ClientThread线程类,用于对接收到服务器的信息,进行处理。如果是登录用户,就添加到用户列表中。

    如果是消息,就追加到文本域中。

    技术重点:

    ​ 在服务器端通过线程对客户端发送的信息进行监听,并对登录用户和消息分别进行处理。如果是登录用户,就将所有用户添加到客户端的用户列表中;如果是消息,就转发给指定的用户;客户端则通过线程对接收到的信息进行处理,如果是登录用户就添加到用户列表中,如果是消息就追加到文本域中。
    ​ (1)在服务器端创建线程类ServerThread,用于对登录用户和消息分别进行处理。如果是登录用户,就将所有用户添加到客户端的用户列表中;如果是消息就转发给指定的用户。

    ​ (2)在客户端创建线程类ClientThread,用于对接收到的信息进行处理,如果是登录用户就添加到用户列表中,如果是消息就追加到文本域中。

    启动多个客户端:

    1、把项目打成jar包:利用maven 的clean install

    如图

    会在target目录下生成jar包

    如图

    2、进入target目录,使用java -cp的命令运行指定的类

    java -cp 命令cp 指的就是classpath。使用命令可以运行jar中的某个指定的类(要包含全路径的包名)

    进入cmd命令模式

    如图

    运行服务端

    java -cp basics98-1.0-SNAPSHOT.jar com.xiaoxuzhu.ClientOneToOneServerFrame

    运行多个客户端

    java -cp basics98-1.0-SNAPSHOT.jar com.xiaoxuzhu.ClientOneToOneClientFrame

    三、代码详解

    ClientOneToOneServerFrame

    package com.xiaoxuzhu;
    import java.awt.BorderLayout;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Hashtable;
    import java.util.Iterator;
    import java.util.Set;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    import javax.swing.JTextArea;
    
    /**
     * Description: 
     *
     * @author xiaoxuzhu
     * @version 1.0
     *
     * <pre>
     * 修改记录:
     * 修改后版本	        修改人		修改日期			修改内容
     * 2022/6/5.1	    xiaoxuzhu		2022/6/5		    Create
     * </pre>
     * @date 2022/6/5
     */
    public class ClientOneToOneServerFrame  extends JFrame{
        private JTextArea ta_info;
        private ServerSocket server; // 声明ServerSocket对象
        private Socket socket; // 声明Socket对象socket
        private Hashtable<String, Socket> map = new Hashtable<String, Socket>();// 用于存储连接到服务器的用户和客户端套接字对象
    
        public void createSocket() {
            try {
                server = new ServerSocket(9527);
                while (true) {
                    ta_info.append("等待新客户连接......\n");
                    socket = server.accept();// 创建套接字对象
                    ta_info.append("客户端连接成功。" + socket + "\n");
                    new ServerThread(socket).start();// 创建并启动线程对象
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        class ServerThread extends Thread {
            Socket socket;
    
            public ServerThread(Socket socket) {
                this.socket = socket;
            }
    
            public void run() {
                try {
                    BufferedReader in = new BufferedReader(new InputStreamReader(
                            socket.getInputStream()));// 创建输入流对象
                    while (true) {
                        String info = in.readLine();// 读取信息
                        String key = "";
                        if (info.startsWith("用户:")) {// 添加登录用户到客户端列表
                            key = info.substring(3, info.length());// 获得用户名并作为键使用
                            map.put(key, socket);// 添加键值对
                            Set<String> set = map.keySet();// 获得集合中所有键的Set视图
                            Iterator<String> keyIt = set.iterator();// 获得所有键的迭代器
                            while (keyIt.hasNext()) {
                                String receiveKey = keyIt.next();// 获得表示接收信息的键
                                Socket s = map.get(receiveKey);// 获得与该键对应的套接字对象
                                PrintWriter out = new PrintWriter(s
                                        .getOutputStream(), true);// 创建输出流对象
                                Iterator<String> keyIt1 = set.iterator();// 获得所有键的迭代器
                                while (keyIt1.hasNext()) {
                                    String receiveKey1 = keyIt1.next();// 获得键,用于向客户端添加用户列表
                                    out.println(receiveKey1);// 发送信息
                                    out.flush();// 刷新输出缓冲区
                                }
                            }
    
                        } else {// 转发接收的消息
                            key = info.substring(info.indexOf(":发送给:") + 5, info
                                    .indexOf(":的信息是:"));// 获得接收方的key值,即接收方的用户名
                            String sendUser = info.substring(0, info
                                    .indexOf(":发送给:"));// 获得发送方的key值,即发送方的用户名
                            Set<String> set = map.keySet();// 获得集合中所有键的Set视图
                            Iterator<String> keyIt = set.iterator();// 获得所有键的迭代器
                            while (keyIt.hasNext()) {
                                String receiveKey = keyIt.next();// 获得表示接收信息的键
                                if (key.equals(receiveKey)
                                        && !sendUser.equals(receiveKey)) {// 如果是发送方,但不是用户本身
                                    Socket s = map.get(receiveKey);// 获得与该键对应的套接字对象
                                    PrintWriter out = new PrintWriter(s
                                            .getOutputStream(), true);// 创建输出流对象
    
                                    out.println("MSG:"+info);// 发送信息
                                    out.flush();// 刷新输出缓冲区
                                }
                            }
                        }
                    }
                } catch (IOException e) {
                    ta_info.append(socket + "已经退出。\n");
                }
            }
        }
    
        /**
         * Launch the application
         *
         * @param args
         */
        public static void main(String args[]) {
            ClientOneToOneServerFrame frame = new ClientOneToOneServerFrame();
            frame.setVisible(true);
            frame.createSocket();
        }
    
        /**
         * Create the frame
         */
        public ClientOneToOneServerFrame() {
            super();
            setTitle("客户端一对一通信——服务器端程序");
            setBounds(100, 100, 385, 266);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            final JScrollPane scrollPane = new JScrollPane();
            getContentPane().add(scrollPane, BorderLayout.CENTER);
    
            ta_info = new JTextArea();
            scrollPane.setViewportView(ta_info);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135

    ClientOneToOneClientFrame

    package com.xiaoxuzhu;
    
    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    import javax.swing.*;
    
    /**
     * Description: 
     *
     * @author xiaoxuzhu
     * @version 1.0
     *
     * <pre>
     * 修改记录:
     * 修改后版本	        修改人		修改日期			修改内容
     * 2022/6/5.1	    xiaoxuzhu		2022/6/5		    Create
     * </pre>
     * @date 2022/6/5
     */
    public class ClientOneToOneClientFrame extends JFrame{
        private JTextField tf_newUser;
        private JList user_list;
        private JTextArea ta_info;
        private JTextField tf_send;
        PrintWriter out;// 声明输出流对象
        private boolean loginFlag = false;// 为true时表示已经登录,为false时表示未登录
        private Socket socket;
        /**
         * Launch the application
         *
         * @param args
         */
        public static void main(String args[]) {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    try {
                        ClientOneToOneClientFrame frame = new ClientOneToOneClientFrame();
                        frame.setVisible(true);
                        frame.createClientSocket();// 调用方法创建套接字对象
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    
        public void createClientSocket() {
            try {
                 socket = new Socket("127.0.0.1", 9527);// 创建套接字对象
                out = new PrintWriter(socket.getOutputStream(), true);// 创建输出流对象
                SwingWorker<Void,Void> worker=new SwingWorker<Void, Void>() {
                    @Override
                    protected Void doInBackground() throws Exception {
                        try {
                            BufferedReader in = new BufferedReader(new InputStreamReader(
                                    socket.getInputStream()));// 创建输入流对象
                            DefaultComboBoxModel model = (DefaultComboBoxModel) user_list
                                    .getModel();// 获得列表框的模型
                            while (true) {
                                String info = in.readLine().trim();// 读取信息
    
                                if (!info.startsWith("MSG:")) {
                                    boolean itemFlag = false;// 标记是否为列表框添加列表项,为true不添加,为false添加
                                    for (int i = 0; i < model.getSize(); i++) {
                                        if (info.equals((String) model.getElementAt(i))) {
                                            itemFlag = true;
                                        }
                                    }
                                    if (!itemFlag) {
                                        model.addElement(info);// 添加列表项
                                    } else {
                                        itemFlag = false;
                                    }
                                } else {
                                    ta_info.append(info + "\n");// 在文本域中显示信息
                                    if (info.equals("88")) {
                                        break;// 结束线程
                                    }
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return null;
                    }
                };
                worker.execute();
                
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        private void send() {
            if (!loginFlag) {
                JOptionPane.showMessageDialog(null, "请先登录。");
                return;
            }
            String sendUserName = tf_newUser.getText().trim();
            String info = tf_send.getText();// 获得输入的信息
            if (info.equals("")) {
                return;// 如果没输入信息则返回,即不发送
            }
            String receiveUserName = (String) user_list.getSelectedValue();// 获得接收信息的用户
            String msg = sendUserName + ":发送给:" + receiveUserName + ":的信息是: "
                    + info;// 定义发送的信息
            if (info.equals("88")) {
                System.exit(0);// 如果没输入信息是88,则退出
            }
            out.println(msg);// 发送信息
            out.flush();// 刷新输出缓冲区
            tf_send.setText(null);// 清空文本框
        }
    
        /**
         * Create the frame
         */
        public ClientOneToOneClientFrame() {
            super();
            setTitle("客户端一对一通信——客户端程序");
            setBounds(100, 100, 385, 288);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            final JPanel panel = new JPanel();
            getContentPane().add(panel, BorderLayout.SOUTH);
    
            final JLabel label = new JLabel();
            label.setText("输入聊天内容:");
            panel.add(label);
    
            tf_send = new JTextField();
            tf_send.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    send();// 调用方法发送信息
                }
            });
            tf_send.setPreferredSize(new Dimension(180, 25));
            panel.add(tf_send);
    
            final JButton button = new JButton();
            button.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    send();// 调用方法发送信息
                }
            });
            button.setText("发  送");
            panel.add(button);
    
            final JSplitPane splitPane = new JSplitPane();
            splitPane.setDividerLocation(100);
            getContentPane().add(splitPane, BorderLayout.CENTER);
    
            final JScrollPane scrollPane = new JScrollPane();
            splitPane.setRightComponent(scrollPane);
    
            ta_info = new JTextArea();
            scrollPane.setViewportView(ta_info);
    
            final JScrollPane scrollPane_1 = new JScrollPane();
            splitPane.setLeftComponent(scrollPane_1);
    
            user_list = new JList();
            user_list.setModel(new DefaultComboBoxModel(new String[] { "" }));
            scrollPane_1.setViewportView(user_list);
    
            final JPanel panel_1 = new JPanel();
            getContentPane().add(panel_1, BorderLayout.NORTH);
    
            final JLabel label_1 = new JLabel();
            label_1.setText("输入用户名称:");
            panel_1.add(label_1);
    
            tf_newUser = new JTextField();
            tf_newUser.setPreferredSize(new Dimension(180, 25));
            panel_1.add(tf_newUser);
    
            final JButton button_1 = new JButton();
            button_1.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    if (loginFlag) {
                        JOptionPane.showMessageDialog(null, "在同一窗口只能登录一次。");
                        return;
                    }
                    String userName = tf_newUser.getText().trim();// 获得登录用户名
                    out.println("用户:" + userName);// 发送登录用户的名称
                    out.flush();// 刷新输出缓冲区
                    tf_newUser.setEnabled(false);
                    loginFlag = true;
                }
            });
            button_1.setText("登  录");
            panel_1.add(button_1);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207

    服务器启动

    如图

    客户端1和客户端2登录

    如图

    客户端小小向客户端虚虚发送消息

    如图

    客户端虚虚向客户端小小发送消息

    如图

    注:小小发给虚虚时,小小自己的界面不显示自己发出的内容。本示例主要是为了演示客户端向指定客户端发送消息。

    多学一个知识点

    swing的开发过程,要了解3种线程的概念:

    • 1、初始化线程 :此类线程将执行初始化应用代码。

    • 2、事件调度线程 :所有的事件处理代码在这里执行。大多数与Swing框架 交互的代码也必须执行这个线程。

      事件调度线程是单线程的:因为 Swing里面的各种组件类,比如JTextField,JButton 都不是线程安全的,这就意味着,如果有多个线程,那么同一个JTextField的setText方法,可能会被多个线程同时调用,这会导致同步问题以及错误数据的发生

    • 3、工作线程 :也称作background threads(后台线程),此类线程将执行所有消耗时间的任务。

      比如的事件监听——在actionPerformed 里放一个长耗时任务,如:数据库访问连接 建立网络连接 文件复制等等
      就会自动进入事件调度线程。 而事件调度线程又是单线程模式,其结果就会是在执行这些长耗时任务的时候,界面就无响应了。

      为了解决这个问题,Swing提供了一个SwingWorker类来解决。 SwingWorker是一个抽象类,为了使用,必须实现方法 doInBackground,在doInBackground中,就可以编写我们的任务,然后执行SwingWorker的execute方法,放在专门的工作线程中去运行。

    上面题目里,ClientOneToOneClientFrame类中的createClientSocket()里就用到了SwingWorker

    四、推荐专栏

    《JAVA从零到壹》

    《JAVA筑基100例》

    五、示例源码下载

    关注下面的公众号,回复筑基+题目号

    筑基98

  • 相关阅读:
    cmd命令行查看mysql数据库命令
    记录--Vue中前端导出word文件
    DETR训练自己数据集心得
    裸辞后进入阿里,斩获34k测开岗offer....
    curl (56) Recv failure Connection reset by peer
    [论文阅读]A ConvNet for the 2020s
    C语言三子棋,五子棋,n子棋的代码实现
    给定一个字符串str,求最长回文子序列长度。
    【AcWing】第 62 场周赛 【2022.07.30】
    Opengl ES之FBO
  • 原文地址:https://blog.csdn.net/shi_hong_fei_hei/article/details/125462962