Java实现局域网聊天--多线程入门

Practice!!!

Posted by eutopia on September 28, 2024

Ai辅助开发Gui界面

好吧,实际上写到这我Java只入门了一周,但秉承着分享和记录的理念,我还是想把这个入门之作写出来,这里也是第一次感受「多线程」的魅力。

正文开始前我先声明一下,

  1. 这里采用的是服务端客户端分离的代码,服务端没有Gui界面,在控制台有部分链接提示,方便信息中转
  2. 这个程序已经可以同时处理多个客户端同时登录的信息,如果有公网ip甚至可以作为一个真正的聊天程序,不必局限于局域网
  3. 代码很简单,并没有联合编程,仅作为一个练习,后续继续完善功能继续再写一篇

前言

上个学期在c++的学习中也学习到了多线程线程锁的概念,但是还没有正式运用到一个较为正式的项目中,这里也算是加深理解的第一个小项目。这里也在强迫自己的代码变得稍微规范一点,不要像之前的屎山代码一样了哈哈哈哈。 这里最中心的逻辑就是不管是客户端还是服务端发送信息都是利用特殊数据流,先输出一个int数字,便于分辨后续信息类型比如客户端先发1,代表接下来是登录消息,比如客户端先发2,代表接下来是群聊消息

服务器端

先来看看主线程吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
        System.out.println("-------------启动服务端系统-----------------");
        try {
            //注册端口
            ServerSocket serverSocket=new ServerSocket(constant.port);
            //主线程负责接收客户端连接请求
            while(true){
                System.out.println("等待客户端连接。。。。");
                Socket socket = serverSocket.accept();
                new ServerReader(socket).start();

                System.out.println("成功接入一个客户端");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这里通过while循环来不断接受新客户端的链接,不断创建子线程来接受处理。

子线程处理

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
 @Override
    public void run(){
        try {
            //接受的消息有很多种类型 1登录类型  2 群聊消息  3 私聊消息
            //所以客户端必须申明协议发送消息
            //比如客户端先发1  代表接下来是登录消息
            //比如客户端先发1  代表接下来是群聊消息
            // 先接收一个数字  在判断,先从输入管道中接受客户端发送来的消息类型编号
            DataInputStream dis=new DataInputStream(socket.getInputStream());
            while (true) {
                int type=dis.readInt();
                switch (type){
                    case 1:
                        //客户端发来了登录消息,再更新全部在线人数列表
                        String nickname=dis.readUTF();
                        name=nickname;
                        String password=dis.readUTF();
                        System.out.println(nickname+"登录了");
                        System.out.println("开始比对--------------");
                        if(!requestiscorrect(nickname,password)){
                            DataOutputStream dos1=new DataOutputStream(socket.getOutputStream());
                            dos1.writeInt(6);//登录失败
                            continue;
                        }
                        DataOutputStream dos1=new DataOutputStream(socket.getOutputStream());
                        dos1.writeInt(7);//登录成功
                        //把这个登录成功的客户端socket存入到在线集合
                        server.onlinesocket.put(socket,nickname);

                        //更新全部客户端在线人数列表
                        updatesocketonlinelist();

                        break;
                    case 2:
                        //客户端发来了群聊消息,再把消息转发给其他所有客户端
                        String msg=dis.readUTF();
                        sendmessagetoall(msg);
                        for (Socket socket : server.onlinesocket.keySet()) {
                            try {
                                //把用户名发送给客户端
                                DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
                                dos.writeInt(2);//1代表告诉客户端接下来是在线人数列表信息    2代表发送群聊消息
                                dos.writeUTF(msg);
                                dos.flush();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        break;
                    case 3:
                        //客户端发来了私聊消息,再把消息转发给指定客户端
                        String to_nickname=dis.readUTF();
                        System.out.println("接收到私聊消息,发送至:"+to_nickname);
                        String msg3=dis.readUTF();
                        //根据用户名查找到相应的管道去发送消息
                        for (Map.Entry<Socket,String> entry : server.onlinesocket.entrySet()) {
                            try {
                                System.out.println(entry.getValue());
                                System.out.println("对比"+to_nickname);
                                if(entry.getValue().equals(to_nickname)){
                                    //把用户名发送给客户端
                                    DataOutputStream dos=new DataOutputStream(entry.getKey().getOutputStream());
                                    System.out.println("匹配成功,正在发送消息");
                                    dos.writeInt(3);//1代表告诉客户端接下来是在线人数列表信息    2代表发送群聊消息
                                    StringBuilder sb2=new StringBuilder();

                                    //获取当前时间
                                    LocalDateTime now=LocalDateTime.now();
                                    DateTimeFormatter dtf=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
                                    String nowstr=dtf.format(now);
                                    //拼接字符串
                                    StringBuilder msgresult=sb2.append(name).append(" ").append(nowstr).append("\r\n").append(msg3).append("\r\n");
                                    dos.writeUTF(msgresult.toString());
                                    dos.flush();
                                }

                            } catch (Exception e) {}
                        }


                        break;
                    case 5:
                        //客户端发来了注册请求,存储到内部文件中
                        String username=dis.readUTF();
                        String userpassword=dis.readUTF();
                        registerforusers(username,userpassword);
                        break;
                    default:
                        break;
                }
            }

        } catch (Exception e) {

            System.out.println("客户端断开连接了"+socket.getInetAddress().getHostAddress());
            server.onlinesocket.remove(socket);
            updatesocketonlinelist();
        }
    }

初步总结

最终,选择使用特殊数据流来收发数据是最为简便和有效的方案。这不仅能够有效地对不同消息进行区分与转发,还便于未来功能的扩展。本文档中仅展示了一个扩展 Thread 类并重写其 run 方法的示例,但从该示例可以看出,核心功能在于持续接收来自客户端的信息,并对其进行处理后转发至其他客户端。

技术细节

  • 字符串拼接:采用 StringBuilder 来提高字符串拼接的速度。
  • 时间记录:通过 LocalDateTime 获取当前时间,以便于日志记录或时间戳的生成。

遇到的问题及解决方案

在数据收发过程中发现,如果不每次操作后都调用 flush() 方法,则可能导致发送或接收失败,进而引起程序崩溃。为了解决这一问题,计划在未来加入线程锁机制以增强程序的安全性。

后续规划

目前,项目处于初步阶段,后续将继续完善,包括但不限于引入线程锁等措施来进一步提升系统的稳定性和安全性。

客户端

项目开发概述

初始构思

项目的初始阶段需要一定的时间进行构思,特别是在 GUI 界面的设计上。尽管我对一些常用的 API 有所了解,但独立开发一个既简单又美观的界面仍需花费不少时间。因此,我选择了使用通义千问来帮助我开发 GUI 框架。

使用通义千问开发 GUI

通义千问的代码逻辑非常清晰,具有良好的扩展性,并且提供了必要的提示,帮助我补充剩余的功能代码。通过多次提出具体要求,最终完成了一个较为美观且支持界面自由切换的 GUI 界面。

注册和登录界面

由于注册和登录界面相对简单,这里不再详细展示。目前,我对 MySQL 仅处于入门阶段,只掌握了部分基础操作,尚不足以实现复杂的联合编程。因此,现阶段仍然使用原始的服务端,通过 txt 文档来存储账号密码数据。

数据同步与处理

服务端在启动后会自动同步读取 txt 文档中的数据,并将其加载到内存中。这样可以方便地对客户端发来的登录和注册请求进行判断和处理。

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
 // 登录按钮监听器
        enterButton.addActionListener(e -> {
            String nickname = nicknameField.getText();
            char[] password = passwordField.getPassword();
            try {
                socket = new Socket(constant.server_ip, constant.port);
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1); // 消息类型  登录
                dos.writeUTF(nickname);
                dos.writeUTF(new String(password)); // 发送密码
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("正在尝试这次登录");
            if(checkissucces(nickname, new String(password))){
                System.out.println("登录成功");
            }else{
                System.out.println("失败的尝试");
                return;
            }
            if (nickname.trim().length() > 0 && password.length > 0) {
                //System.out.println("昵称: " + nickname);
                //System.out.println("密码: " + new String(password)); // 注意:实际应用中不要直接打印密码
                try {
                    clientLogin(nickname, new String(password));
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                ChatWindow chatWindow = new ChatWindow(nickname, socket);
                chatWindow.setVisible(true);
                dispose();
            } else {
                JOptionPane.showMessageDialog(ChatEntryForm.this, "请输入您的昵称和密码!", "错误", JOptionPane.ERROR_MESSAGE);
            }
        });
        //注册界面按键
        registerButton.addActionListener(e -> {
            RegisterForm registerForm = new RegisterForm(this,socket); // 传入当前登录窗体实例
            registerForm.setVisible(true);
        });
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
 private boolean checkissucces(String nickname, String s) {
        try {
                DataInputStream dis = new DataInputStream(socket.getInputStream());
                int type = dis.readInt();
                if (type == 6) {//登录失败
                    JOptionPane.showMessageDialog(ChatEntryForm.this, "用户名或密码错误,请重新输入!", "错误", JOptionPane.ERROR_MESSAGE);
                    return false;
                } else {
                    return true;
                }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;

    }

    private boolean clientLogin(String nickname, String password) throws Exception {
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        dos.writeInt(1); // 消息类型  登录
        dos.writeUTF(nickname);
        dos.writeUTF(password); // 发送密码
        dos.flush();
        return  true;
    }

好了到这里就可以开始真正的聊天界面的代码,这里并没有使用循环创建多线程,客户端都是运行在本地的一个信息收发端,只用一个子线程并发就行了,

1
2
3
4
5
6
// hypothetical Vue lang
component Counter (props) {    // constructor
  @reactive count = 0          // field
  inc() { count.value ++ }     // method
  render() { return <a onClick={inc}>{count.value}</a> }
}

到这里发现其实感觉还是太简单了,没有私聊怎么算是聊天项目呢是吧哈哈哈,然后又创建一个新的类,为了能最大程度延用之前的框架,选择不新创建窗口而是直接扩展这个现有的窗口

私聊界面

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
public class PrivateChatPanel extends JPanel {
    private JTextArea privateMessageArea;
    private JTextField privateMessageField;
    private JButton privateSendButton;
    private JButton backButton;
    private String currentPrivateChatUser;
    private Socket socket;
    private ChatWindow chatWindow;

    public PrivateChatPanel(Socket socket, ChatWindow chatWindow) {
        this.socket = socket;
        this.chatWindow = chatWindow;
        initialize();
    }

    private void initialize() {
        setLayout(new BorderLayout());

        // 私聊消息显示区域
        privateMessageArea = new JTextArea();
        privateMessageArea.setEditable(false);
        privateMessageArea.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        privateMessageArea.setLineWrap(true);
        privateMessageArea.setWrapStyleWord(true);
        JScrollPane privateMessageScrollPane = new JScrollPane(privateMessageArea);

        // 输入面板
        JPanel privateInputPanel = new JPanel();
        privateInputPanel.setBackground(Color.WHITE);
        privateInputPanel.setLayout(new BorderLayout());

        privateMessageField = new JTextField();
        privateMessageField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        privateInputPanel.add(privateMessageField, BorderLayout.CENTER);

        privateSendButton = new JButton("私聊");
        privateSendButton.setFont(new Font("微软雅黑", Font.BOLD, 14));
        privateSendButton.setBackground(new Color(0x4CAF50));
        privateSendButton.setForeground(Color.WHITE);
        privateSendButton.setFocusPainted(false);
        privateInputPanel.add(privateSendButton, BorderLayout.EAST);

        add(privateMessageScrollPane, BorderLayout.CENTER);
        add(privateInputPanel, BorderLayout.SOUTH);

        // 返回按钮
        backButton = new JButton("当前为私聊界面,点击返回主界面");
        backButton.setFont(new Font("微软雅黑", Font.BOLD, 14));
        backButton.setBackground(new Color(0xF44336));
        backButton.setForeground(Color.WHITE);
        backButton.setFocusPainted(false);
        backButton.addActionListener(e -> chatWindow.switchToGroupChat());

        JPanel buttonPanel = new JPanel();
        buttonPanel.setBackground(Color.WHITE);
        buttonPanel.add(backButton);
        add(buttonPanel, BorderLayout.NORTH);

        // 按钮监听器
        privateSendButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                String message = privateMessageField.getText();
                privateMessageField.setText("");
                // 发送私聊消息
                StringBuilder sb2=new StringBuilder();
                LocalDateTime now=LocalDateTime.now();
                DateTimeFormatter dtf=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
                String nowstr=dtf.format(now);
                //拼接字符串
                StringBuilder msgresult=sb2.append("").append(" ").append(nowstr).append("\r\n").append(message).append("\r\n").append("\r\n");

                privateMessageArea.append(msgresult.toString());
                sendMessageToServer(3, message);
            }
        });
    }

    public void setCurrentPrivateChatUser(String user) {
        this.currentPrivateChatUser = user;
        privateMessageArea.setText(""); // 清空私聊消息区域
    }

    private void sendMessageToServer(int type, String message) {
        try {
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            dos.writeInt(type);
            if (type == 3) {
                dos.writeUTF(currentPrivateChatUser); // 目标用户
            }
            dos.writeUTF(message);
            dos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public JTextArea getPrivateMessageArea() {
        return privateMessageArea;
    }

    public String getCurrentPrivateChatUser() {
        return currentPrivateChatUser;
    }
}
}

补充

这个项目最多是为了练习多线程,再加上一些读写数据流和数组的练习,总体难度不是很高,但是对于我这初学者还是需要不断的试错选择更合理的方案了。 总结一下最后还是没有实现能够收发图片,这点很遗憾,我也会在后续学完mysql后再回来优化这个代码,加上保存聊天界面,个人信息等等,到此先告一段落

参考

  1.  通义千问