Java Socket 多人聊天室(私聊+群聊)技术笔记(V4版本)

一、项目整体架构

本项目是基于 BIO 阻塞式 Socket 实现的桌面多人聊天室,支持用户登录、群聊、一对一私聊功能,搭配Swing实现可视化客户端界面。整体分为服务端、客户端、用户实体、UI界面四大核心模块,采用 服务端中转消息 的通信模式(所有客户端消息统一发送至服务端,由服务端分发转发)。

1.1 核心模块分工

  • Server(服务端):监听端口、接收客户端连接、处理用户登录、解析消息类型、转发私聊/群聊消息、维护在线用户列表
  • Client(客户端):建立服务端连接、用户账号登录、封装私聊/群聊消息、发送消息、接收服务端转发的消息
  • ClientUser(用户实体):封装在线用户信息(账号、Socket连接),服务端核心存储对象
  • ClientUI(可视化界面):基于Swing实现聊天窗口、输入框、发送按钮,绑定消息收发事件

1.2 通信核心原理

基于TCP长连接通信,客户端与服务端建立永久Socket连接,全程流式传输字节数据;通过 自定义消息协议 区分私聊、群聊消息,服务端统一调度转发,实现多客户端通信。

二、自定义通信协议(核心重点)

项目无第三方通信框架,完全自定义消息格式,通过固定字节顺序解析消息类型、发送目标、消息内容,是实现私聊/群聊区分的核心。

2.1 消息类型定义

  • 1:私聊消息(定向发送指定用户)
  • 2:群聊消息(广播给所有在线用户,除发送者自身)

2.2 完整消息协议格式

私聊消息格式:消息类型(1字节) + 好友名称长度(1字节) + 好友名称(字节数组) + 消息长度(1字节) + 消息内容(字节数组)

群聊消息格式:消息类型(1字节) + 消息长度(1字节) + 消息内容(字节数组)

登录消息格式:账号长度(1字节) + 账号名称(字节数组)

三、用户实体类(ClientUser)

服务端用于存储所有在线用户的核心实体,绑定用户账号与Socket连接,实现精准消息转发。

java 复制代码
package V4;
import java.net.Socket;

/**
 * 在线用户实体类:封装单个客户端的连接信息与账号
 * 服务端集合通过该对象维护所有在线用户
 */
public class ClientUser {
    String account; // 客户端登录账号
    Socket socket;  // 客户端对应的Socket连接对象

    // 构造方法:登录成功后创建用户对象
    public ClientUser(Socket socket, String account) {
        this.account = account;
        this.socket = socket;
    }
}

四、客户端核心实现(Client)

客户端核心功能:初始化连接、账号登录、封装发送消息、接收服务端返回消息。所有IO操作基于字节流传输,严格遵循自定义协议。

java 复制代码
package V4;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * 聊天室客户端核心类
 * 负责连接服务端、登录、消息发送、消息接收
 */
public class Client {
    Socket socket = null; // 客户端Socket连接对象

    // 初始化:连接服务端 + 执行登录逻辑
    public void init(){
        try {
            // 连接本地服务端,端口与服务端一致:54321
            socket = new Socket("127.0.0.1",54321);
            login(socket.getOutputStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 用户登录:向服务端发送账号信息
    public void login(OutputStream os){
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入账号:");
        String account = sc.nextLine();
        byte[] accounts = account.getBytes();
        try {
            // 先发送账号长度,再发送账号内容(服务端按长度读取,避免粘包)
            os.write(accounts.length);
            os.write(accounts);
            os.flush(); // 强制刷新流,确保数据立即发送
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 消息发送核心方法:区分私聊/群聊,封装协议数据
     * 规则:消息以@开头为私聊,否则为群聊
     */
    public void send(String message){
        try {
            OutputStream os = socket.getOutputStream();
            // 判断消息类型:首字符@ 代表私聊
            if (message.charAt(0) == '@') {
                // 拆分私聊格式:@好友名 消息内容
                int index = message.indexOf(' ');
                String friend = message.substring(1, index); // 截取目标好友账号
                String msg = message.substring(index + 1);   // 截取消息内容

                // 按照私聊协议封装数据
                os.write(1);// 1=私聊类型标识
                int fidlen = friend.getBytes().length;
                os.write(fidlen); // 好友名称字节长度
                os.write(friend.getBytes()); // 好友名称字节数据
                int msglen = msg.getBytes().length;
                os.write(msglen); // 消息内容字节长度
                os.write(msg.getBytes()); // 消息内容字节数据
            }else{
                // 群聊消息封装
                os.write(2);// 2=群聊类型标识
                byte[] msgs = message.getBytes();
                os.write(msgs.length); // 消息长度
                os.write(msgs); // 消息内容
            }
            os.flush();// 强制刷新,完成消息发送
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 读取服务端转发的消息
    public String readMsg(){
        try {
            InputStream ins = socket.getInputStream();
            // 先读长度,再读内容,解决TCP粘包问题
            int len = ins.read();
            byte[] msgs = new byte[len];
            ins.read(msgs);
            return new String(msgs);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static void main() {
    }
}

4.1 客户端核心关键点备注

  • 粘包解决核心:所有消息传输遵循「先传长度、再传内容」,服务端根据长度精准读取字节,避免TCP流式传输导致的粘包、半包问题
  • 消息区分规则:通过消息首字符@判定私聊,空格分割好友名称与消息内容
  • 流刷新机制:每次write后必须执行flush(),否则数据会滞留缓冲区,无法及时发送

五、服务端核心实现(Server)

服务端是整个聊天室的中枢,负责监听连接、处理登录、解析消息、转发消息、维护在线用户列表,通过多线程支持多客户端同时在线。

java 复制代码
package V4;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

/**
 * 聊天室服务端核心类
 * 端口监听、客户端连接管理、消息解析、消息转发
 */
public class Server {
    ServerSocket ss = null;
    // 全局在线用户列表:存储所有已登录的客户端连接
    ArrayList<ClientUser> socketlist<>();

    // 初始化服务端:绑定固定端口54321
    public void init() {
        try {
            ss = new ServerSocket(54321);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 监听客户端连接:阻塞方法,等待客户端接入
    public Socket listen() {
        try {
            return ss.accept();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 统一消息发送工具:服务端向客户端转发消息
    public void send(String message, OutputStream os){
        try {
            // 遵循长度+内容协议发送消息
            int len = message.getBytes().length;
            os.write(len);
            os.write(message.getBytes());
            os.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 消息解析方法:读取客户端发送的原始字节流
     * 根据消息类型(1/2)分别解析私聊、群聊数据
     * 返回带标识的字符串,用于后续转发逻辑处理
     */
    public String receive(InputStream in) {
        try {
            int type = in.read(); // 读取消息类型
            if(type == 1){
                // 解析私聊消息
                int fidLen = in.read (); // 好友名称长度
                byte [] fid = new byte [fidLen];
                in.read (fid);
                String fidStr = new String(fid); // 目标好友账号

                int mlen = in.read (); // 消息长度
                byte [] msgs = new byte [mlen];
                in.read (msgs);
                String msg = new String (msgs); // 消息内容
                // 自定义分隔符# 拼接数据,方便后续拆分处理
                return "1"+"#"+fidStr+"#"+ msg;
            }else {
                // 解析群聊消息
                int len = in.read();
                byte[] msgs = new byte[len];
                in.read(msgs);
                String msg = new String(msgs);
                return "2"+"#"+msg;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    /**
     * 处理客户端所有消息的核心方法
     * 每个客户端连接单独开启一个子线程,实现多客户端并发
     */
    public void proClientMsg(Socket socket) {
        new Thread(() -> {
            // 1. 处理用户登录,获取登录账号
            String account = prologin(socket);
            // 2. 将在线用户存入集合
            ClientUser clientUser = new ClientUser(socket,account);
            socketlist.add(clientUser);

            // 3. 循环监听当前客户端的所有消息
            while (true) {
                try {
                    InputStream in = clientUser.socket.getInputStream();
                    String msg = receive(in);
                    String[] datas=msg.split("#"); // 拆分解析后的消息数据
                    String type = datas[0];

                    // 私聊消息转发逻辑
                    if(type.equals("1")){
                        String fid = datas[1]; // 目标好友账号
                        String msg1 = datas[2]; // 消息内容
                        // 遍历在线用户列表,精准匹配目标用户
                        for (ClientUser otCU : socketlist) {
                            if(otCU.account.equals(fid)){
                                // 拼接发送者信息,转发给目标用户
                                msg1 = clientUser.account+"说:"+msg1;
                                send(msg1,otCU.socket.getOutputStream());
                                break;
                            }
                        }
                    }else {
                        // 群聊消息转发逻辑:广播给所有在线用户(排除自己)
                        String msg1 = datas[1];
                        msg1 = clientUser.account+"说:"+msg1;
                        for (ClientUser otCU : socketlist) {
                            Socket socket1 = otCU.socket;
                            if(socket1 != clientUser.socket){
                                send(msg1,socket1.getOutputStream());
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

    // 处理用户登录:读取客户端发送的账号信息
    public String prologin(Socket socket) {
        try {
            InputStream in = socket.getInputStream();
            int len = in.read();
            byte[] msgs = new byte[len];
            in.read(msgs);
            String account = new String (msgs);
            System.out.println ("用户名:"+account+" 上线了 ");
            return account;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 服务端启动入口
    static void main() {
        Server server = new Server();
        server.init();
        // 循环监听客户端连接(主线程阻塞,单独开启线程)
        new Thread(()->{
            while (true){
                System.out.println("监听连接中~");
                Socket s = server.listen();
                System.out.println(s.getInetAddress()+":"+s.getPort()+"连接成功!");
                server.proClientMsg(s); // 处理该客户端的所有后续逻辑
            }
        }).start();
    }
}

5.1 服务端核心关键点备注

  • 多线程并发:每个客户端连接单独创建子线程处理消息,避免单线程阻塞,支持多用户同时在线
  • 消息转发机制:所有消息不直接点对点传输,必须经过服务端中转,实现统一管理
  • 私聊精准匹配:通过遍历在线用户集合,匹配账号名称,实现定向消息发送
  • 群聊广播过滤:转发群聊消息时排除发送者自身,避免自己收到自己的消息
  • 数据分隔处理:使用#作为自定义分隔符,拆分消息类型、目标账号、消息内容,解析简单高效

六、可视化UI界面实现(ClientUI)

基于Swing实现桌面可视化窗口,包含聊天记录展示区、消息输入框、发送按钮,同时开启子线程实时接收服务端消息,避免UI阻塞。

java 复制代码
package V4;

import javax.swing.*;
import java.awt.*;

/**
 * 聊天室可视化客户端界面
 * 基于Swing实现,绑定消息收发事件
 */
public class ClientUI extends JFrame {
    Client client = new Client(); // 绑定客户端核心通信对象

    public static void main() {
        ClientUI clientUI = new ClientUI();
        clientUI.showUI();
    }

    // 初始化展示UI窗口
    public void showUI() {
        // 启动客户端连接+登录
        client.init();

        // 窗口基础配置
        JFrame jf = new JFrame("小小聊天室");
        jf.setSize(500, 550);
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.setLocationRelativeTo(null); // 窗口居中

        // 聊天记录展示区域(带滚动条)
        JTextArea msgArea = new JTextArea();
        JScrollPane scrollPane = new JScrollPane(msgArea);
        scrollPane.setPreferredSize(new Dimension(0, 470));

        // 消息输入框、发送按钮
        JTextField msgField = new JTextField();
        msgField.setBackground(Color.pink);
        JButton sendBtn = new JButton("发送");
        msgField.setPreferredSize(new Dimension(370, 0));
        sendBtn.setPreferredSize(new Dimension(100, 0));

        // 组件布局添加
        jf.add(scrollPane, BorderLayout.NORTH);
        jf.add(msgField, BorderLayout.WEST);
        jf.add(sendBtn, BorderLayout.EAST);
        jf.setVisible(true);

        // 绑定发送按钮点击事件
        sendBtn.addActionListener(e -> sendMsg(msgArea,msgField));

        // 开启子线程:实时接收服务端消息,刷新UI(防止主线程阻塞)
        new Thread(() -> {
            while (true) {
                String msg = client.readMsg();
                msgArea.append(msg+"\n"); // 追加消息到聊天界面
            }
        }).start();
    }

    // 发送消息事件:点击按钮触发
    public void sendMsg(JTextArea msgArea, JTextField msgField) {
        String msg = msgField.getText();
        // 本地先展示自己发送的消息
        msgArea.append("我说:"+msg+"\n");
        // 调用客户端方法发送消息到服务端
        client.send(msg);
        msgField.setText(""); // 清空输入框
    }
}

6.1 UI核心关键点备注

  • 子线程接收消息:消息接收是阻塞循环操作,必须放在子线程,否则会卡死Swing主线程(UI刷新线程)
  • 本地消息回显:自己发送的消息本地直接展示,服务端只转发他人消息,提升体验
  • 滚动文本域:使用JScrollPane包裹文本域,实现聊天记录滚动查看

七、项目启动流程总结

  1. 启动 Server服务端,监听54321端口,等待客户端连接
  2. 启动 ClientUI客户端,输入账号完成登录,服务端记录在线用户
  3. 客户端输入消息:无@为群聊,@好友+空格+内容为私聊
  4. 客户端封装协议消息发送至服务端
  5. 服务端解析消息类型,转发至对应目标用户
  6. 客户端子线程接收转发消息,刷新UI展示聊天内容
相关推荐
龙仔7251 小时前
人大金仓OS_Core数据库自动备份实施笔记(银河麒麟Linux)
linux·数据库·笔记·备份·人大金仓
wang09071 小时前
自己动手写一个spring之aop_1
java·后端·spring
泡沫冰@2 小时前
上章节中文件的讲解
前端·网络·nginx
国科安芯2 小时前
FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器
java·开发语言·单片机·嵌入式硬件·算法·系统架构·risc-v
deng12042 小时前
【Thread类的用法(1)-- Thread类的基本用法】
java·java-ee
suconnect3 小时前
Spring Boot接入企业RAG:文档切分、向量检索、权限过滤和答案溯源
java·spring boot·后端
weixin_BYSJ19873 小时前
springboot校园自习室管理小程序---附源码32142
java·javascript·spring boot·python·django·flask·php
ClickHouseDB3 小时前
ClickHouse托管Postgres:OLTP+OLAP,新能力解锁最佳数据平台
java·前端·数据库
霸道流氓气质3 小时前
Java中集成Smile 技术教程:从入门到工程实践
java·开发语言