基于 Java Socket 实现多人聊天室系统(附完整源码)

在网络编程学习中,Socket(套接字)是实现 TCP/IP 通信的核心基础。本文将手把手教你搭建一个支持注册、登录、私聊、群发、在线用户查询 的多人聊天室系统,全程基于 Java 原生 Socket 和 Swing 实现,无第三方框架依赖,适合 Java 网络编程入门学习。

一、系统整体设计

1. 核心功能

  • 用户注册 / 登录(支持账号唯一性校验、登录状态防重复)
  • 私聊功能:指定在线用户发送一对一消息
  • 群发功能:向所有在线用户广播消息
  • 在线用户查询:展示当前登录的所有用户列表
  • 安全退出:注销登录状态,释放连接资源

2. 技术栈

  • 网络通信:Java Socket(TCP 协议)
  • 多线程:Thread/Runnable 处理多客户端并发
  • UI 界面:Swing 实现图形化交互
  • 数据存储:集合(ArrayList/HashMap)存储用户、连接信息

3. 架构设计

采用经典的C/S(客户端 - 服务器)架构

  • 服务端(Server):监听 9999 端口,为每个客户端创建独立线程(SeverThread),处理登录、注册、消息转发等核心逻辑
  • 客户端(Client):分为登录 UI(GameUI)、聊天 UI(chatUI),负责用户交互和消息收发

二、核心代码拆解

1. 公共常量定义(Message.java)

首先定义指令常量,统一客户端和服务端的指令交互格式,避免硬编码:

java 复制代码
package yjq0125.Sever;

public class Message {
    public static final int login=1;      //登录指令
    public static final int register=2;   //注册指令
    public static final int chatprivate=3;//私聊指令
    public static final int chatall=4;    //群发指令
    public static final int logout=5;     //退出指令
    public static final int search=6;    //查询在线用户指令
}

2. 用户实体类(User.java)

封装用户核心信息,包含账号、密码、用户名(展示用):

java 复制代码
package yjq0125.Sever;

public class User {
    String account;   //账号
    String password;  //密码
    int id;           //预留ID
    String idname;    //用户名(展示名)
    
    //空构造
    public User(){}

    //带参构造(注册时使用)
    public User(String account,String password,String idname) {
        this.account=account;
        this.password = password;
        this.idname=idname;
    }
}

3. 服务端核心实现

(1)服务端启动类(Msever.java)

创建 ServerSocket 监听端口,接收客户端连接并创建独立线程处理:

java 复制代码
package yjq0125.Sever;

import java.net.ServerSocket;
import java.net.Socket;

public class Msever {
    public static void main(String[] args) throws Exception {
        Msever msever=new Msever();
        msever.startSever();
    }

    public void startSever() throws Exception {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("启动服务器,监听9999端口...");
        int ID = 1;
        while (true) {
            // 阻塞等待客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("客户端"+ID+++"已连接");
            // 为每个客户端创建独立线程,避免单线程阻塞
            SeverThread severThread=new SeverThread(socket);
            new Thread(severThread).start();
        }
    }
}
(2)服务端线程类(SeverThread.java)

核心逻辑类,处理客户端的所有指令请求,关键要点:

  • ArrayList<User>存储所有注册用户
  • HashMap<String, Socket>维护在线用户的 Socket 连接
  • HashMap<String, Boolean>标记用户登录状态,防止重复登录
  • 所有共享集合操作加synchronized保证线程安全
java 复制代码
package yjq0125.Sever;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class SeverThread implements Runnable {
    Socket socket;//当前客户端套接字
    OutputStream os;//输出流
    InputStream is;//输入流

    public static ArrayList<User> userArrList = new ArrayList<>();//所有注册用户
    boolean iflogin = false;//当前客户端是否登录
    String id;//当前登录的用户名
    public static Map<String, Socket> socketmap = new HashMap<>();//在线用户-Socket映射
    public static Map<String, Boolean> checklogin = new HashMap<>();//用户-登录状态映射

    // 构造方法:初始化Socket和IO流
    SeverThread(Socket socket) throws IOException {
        this.socket = socket;
        os = socket.getOutputStream();
        is = socket.getInputStream();
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 读取客户端指令
                String s = readmes();
                String[] a = s.split(":");
                int choice = Integer.parseInt(a[0]);
                switch (choice) {
                    // 1. 登录逻辑
                    case Message.login: {
                        String[] mes = a[1].split(",");
                        String account = mes[0];
                        String password = mes[1];
                        boolean ifnameright = false;
                        boolean ifpasswordright = false;
                        User user1 = new User();
                        // 同步集合,防止多线程并发修改
                        synchronized (userArrList) {
                            // 校验账号是否存在
                            for (User user : userArrList) {
                                if (user.account.equals(account)) {
                                    ifnameright = true;
                                    user1 = user;
                                    break;
                                }
                            }
                            if (!ifnameright) {
                                sendMes(socket, "账号错误登录失败");
                            } else {
                                // 校验密码
                                if (user1.password.equals(password)) {
                                    ifpasswordright = true;
                                }
                                if (ifpasswordright) {
                                    // 校验是否已登录
                                    Boolean loginSatus = checklogin.get(account);
                                    if (Boolean.TRUE.equals(loginSatus)) {
                                        sendMes(socket, "账号已登录,登录失败");
                                    } else {
                                        id = user1.idname;
                                        checklogin.put(id, true);
                                        socketmap.put(id, socket);
                                        sendMes(socket, "登录成功");
                                        iflogin = true;
                                    }
                                } else {
                                    sendMes(socket, "密码错误登录失败");
                                }
                            }
                        }
                        break;
                    }
                    // 2. 注册逻辑
                    case Message.register: {
                        String[] mes = a[1].split(",");
                        String account = mes[0];
                        String password = mes[1];
                        String idname = mes[2];
                        boolean ifsuccess = true;
                        synchronized (userArrList) {
                            // 校验账号是否已存在
                            for (User user : userArrList) {
                                if (user.account.equals(account)) {
                                    ifsuccess = false;
                                    break;
                                }
                            }
                            if (ifsuccess) {
                                userArrList.add(new User(account, password, idname));
                                sendMes(socket, "注册成功");
                            } else {
                                sendMes(socket, "账号已存在");
                            }
                        }
                        break;
                    }
                    // 3. 私聊逻辑
                    case Message.chatprivate: {
                        if (!iflogin) {
                            sendMes(socket, "没有登录成功不能发送消息");
                        } else {
                            String[] mes = a[1].split(",");
                            String idname = mes[0];
                            String message = "["+id+"]"+"私聊:"+mes[1];
                            Socket socket1 = null;
                            // 查找目标用户的Socket
                            for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
                                if (idname.equals(entry.getKey())) {
                                    socket1 = entry.getValue();
                                    break;
                                }
                            }
                            if(socket1 != null) sendMes(socket1, message);
                        }
                        break;
                    }
                    // 4. 群发逻辑
                    case Message.chatall: {
                        if (!iflogin) {
                            sendMes(socket, "没有登录成功不能发送消息");
                        } else {
                            String mes = "[" + id + "]群发:" + a[1];
                            synchronized (socketmap) {
                                // 遍历所有在线用户,排除自己
                                for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
                                    Socket socket1 = entry.getValue();
                                    if (!socket.equals(socket1)) {
                                        sendMes(socket1, mes);
                                    }
                                }
                            }
                        }
                        break;
                    }
                    // 5. 退出登录
                    case Message.logout: {
                        synchronized (checklogin) {
                            if (!iflogin) {
                                sendMes(socket, "未登录,无需退出");
                                break;
                            }
                            if (checklogin.get(id)) {
                                checklogin.put(id, false);
                                socketmap.remove(id);
                                sendMes(socket, "退出登录成功");
                                iflogin = false;
                            }
                        }
                        break;
                    }
                    // 6. 查询在线用户
                    case Message.search: {
                        String idlist = "";
                        synchronized (socketmap) {
                            for (Map.Entry<String, Socket> entry : socketmap.entrySet()) {
                                idlist += (entry.getKey() + ",");
                            }
                            sendMes(socket, idlist);
                        }
                        break;
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    // 读取客户端消息(处理粘包/空字符问题)
    public String readmes() throws Exception {
        byte[] b = new byte[1024];
        int actualLen = is.read(b);
        if (actualLen == -1) {
            throw new IOException("服务器连接已断开");
        }
        String mes = new String(b, 0, actualLen, StandardCharsets.UTF_8);
        return mes.replace("\r\n", "").trim();
    }

    // 发送消息给客户端(添加换行符,方便客户端识别)
    public void sendMes(Socket socket, String mes) throws Exception {
        OutputStream outputStream = socket.getOutputStream();
        String sendStr = mes + "\r\n";
        outputStream.write(sendStr.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
    }
}

4. 客户端实现

(1)客户端通信类(MClient.java)

封装 Socket 连接、消息读写、退出逻辑,作为客户端和服务端的通信桥梁:

java 复制代码
package yjq0125.Client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class MClient {
    public OutputStream os;
    public InputStream is;
    public boolean iflogin = false;
    Socket client;
    Map<String,String> map=new HashMap<>();//存储账号-用户名映射

    // 启动客户端,连接服务端
    public void startClient() throws Exception {
        client = new Socket("127.0.0.1", 9999);
        os = client.getOutputStream();
        is = client.getInputStream();
    }

    // 读取服务端消息
    public String readmes() throws Exception {
        byte[] b = new byte[1024];
        int actualLen = is.read(b);
        if (actualLen == -1) {
            throw new IOException("服务器连接已断开");
        }
        String mes = new String(b, 0, actualLen, StandardCharsets.UTF_8);
        return mes.replace("\r\n", "").trim();
    }

    // 发送消息到服务端
    public void writemes(String s) throws Exception {
        String str = s + "\r\n";
        os.write(str.getBytes());
        os.flush();
    }

    // 退出登录并关闭连接
    public void logout()throws Exception {
        if (iflogin) {
            writemes("5:");
            iflogin = false;
        }
        if(os!=null)os.close();
        if(is!=null)is.close();
        if(client!=null)client.close();
    }
}
(2)登录界面(GameUI.java)

基于 Swing 实现登录 / 注册 UI,核心要点:

  • 子线程处理网络请求,避免 UI 阻塞
  • Swing 组件更新必须在 EDT 线程(SwingUtilities.invokeAndWait
  • 输入校验(非空判断)
java 复制代码
package yjq0125.Client;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class GameUI {
    JTextField account;
    JTextField password;
    JTextField idname ;
    MClient mClient;
    JFrame jFrame;

    GameUI() throws Exception {
        mClient = new MClient();
        mClient.startClient();
    }

    public static void main(String[] args) throws Exception {
        GameUI gameUI = new GameUI();
        gameUI.initUI();
    }

    public void initUI() {
        jFrame = new JFrame();
        jFrame.setTitle("登录界面");
        jFrame.setSize(500, 600);
        jFrame.setLocationRelativeTo(null);
        jFrame.setDefaultCloseOperation(3);
        FlowLayout flow = new FlowLayout();
        jFrame.setLayout(flow);
        
        // 背景图片(可自行替换路径)
        ImageIcon imageIcon = new ImageIcon("photo/01.jpg");
        Image target = imageIcon.getImage().getScaledInstance(500, 400, Image.SCALE_SMOOTH);
        JLabel jLabel = new JLabel(new ImageIcon(target));
        jFrame.add(jLabel);
        
        // 账号输入框
        JLabel usernameLabel = new JLabel("账号:");
        jFrame.add(usernameLabel);
        account = new JTextField();
        account.setPreferredSize(new Dimension(420, 30));
        jFrame.add(account);
        
        // 密码输入框
        JLabel pwdLabel = new JLabel("密码:");
        jFrame.add(pwdLabel);
        password = new JTextField();
        password.setPreferredSize(new Dimension(420, 30));
        jFrame.add(password);
        
        // 用户名输入框(注册用)
        JLabel idnameLabel = new JLabel("用户名:(登录时不用填)");
        jFrame.add(idnameLabel);
        idname = new JTextField();
        idname.setPreferredSize(new Dimension(280, 30));
        jFrame.add(idname);
        
        // 登录按钮
        JButton login = new JButton("登录");
        jFrame.add(login);
        // 注册按钮
        JButton register = new JButton("注册");
        jFrame.add(register);

        // 登录按钮事件
        login.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String username = account.getText();
                String userpassword = password.getText();
                if (username.trim().equals("") || userpassword.trim().equals("")) {
                    JOptionPane.showMessageDialog(jFrame, "账号和密码不能为空", "提示", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 子线程处理网络请求
                new Thread(() -> {
                    try {
                        mClient.writemes("1:" + username + "," + userpassword);
                        String mes = mClient.readmes();
                        // EDT线程更新UI
                        SwingUtilities.invokeAndWait(() -> {
                            account.setText("");
                            password.setText("");
                            if (mes.equals("登录成功")) {
                                JOptionPane.showMessageDialog(jFrame, mes, "登录成功", JOptionPane.INFORMATION_MESSAGE);
                                mClient.iflogin = true;
                                // 打开聊天界面
                                new chatUI(username, mClient).initui();
                            } else {
                                JOptionPane.showMessageDialog(jFrame, mes, "登录失败", JOptionPane.ERROR_MESSAGE);
                            }
                        });
                    } catch (Exception ex) {
                        try {
                            SwingUtilities.invokeAndWait(() -> {
                                JOptionPane.showMessageDialog(jFrame, "操作失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
                            });
                        } catch (Exception innerEx) {
                            innerEx.printStackTrace();
                        }
                        ex.printStackTrace();
                    }
                }).start();
            }
        });

        // 注册按钮事件
        register.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String username = account.getText();
                String userpassword = password.getText();
                String idnameText = idname.getText();
                if (username.trim().equals("") || userpassword.trim().equals("") || idnameText.trim().equals("")) {
                    JOptionPane.showMessageDialog(jFrame, "账号、密码、用户名不能为空", "提示", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                new Thread(() -> {
                    try {
                        mClient.writemes("2:" + username + "," + userpassword + "," + idnameText);
                        String mes = mClient.readmes();
                        SwingUtilities.invokeAndWait(() -> {
                            account.setText("");
                            password.setText("");
                            idname.setText("");
                            if (mes.equals("注册成功")) {
                                JOptionPane.showMessageDialog(jFrame, mes, "提示", JOptionPane.INFORMATION_MESSAGE);
                                mClient.map.put(username, idnameText);
                            } else {
                                JOptionPane.showMessageDialog(jFrame, mes, "提示", JOptionPane.ERROR_MESSAGE);
                            }
                        });
                    } catch (Exception ex) {
                        try {
                            SwingUtilities.invokeAndWait(() -> {
                                JOptionPane.showMessageDialog(jFrame, "注册失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
                            });
                        } catch (Exception innerEx) {
                            innerEx.printStackTrace();
                        }
                        ex.printStackTrace();
                    }
                }).start();
            }
        });
        jFrame.setVisible(true);
    }
}
(3)聊天界面(chatUI.java)

实现消息收发、在线用户查询、私聊 / 群发切换:

java 复制代码
package yjq0125.Client;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.atomic.AtomicBoolean;

public class chatUI {
    public String idname;
    public MClient mClient;
    JTextArea jTextArea;
    private DefaultListModel<String> onlineUserModel;
    private JList<String> onlineUserList;
    private AtomicBoolean isSearching = new AtomicBoolean(false);

    chatUI(String idname, MClient mClient) {
        this.idname = idname;
        this.mClient = mClient;
        onlineUserModel = new DefaultListModel<>();
        onlineUserList = new JList<>(onlineUserModel);
        onlineUserList.setFont(new Font("楷体", Font.PLAIN, 18));
        onlineUserList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    }

    public void initui() {
        JFrame jf = new JFrame(idname);
        jf.setSize(new Dimension(600, 600));
        jf.setDefaultCloseOperation(3);
        jf.setLocationRelativeTo(null);

        // 中间面板:消息展示+输入
        JPanel centerjPanel = new JPanel();
        jf.add(centerjPanel, BorderLayout.CENTER);
        
        // 左侧面板:在线用户查询+列表
        JPanel westjPanel = new JPanel();
        westjPanel.setLayout(new BoxLayout(westjPanel, BoxLayout.Y_AXIS));
        westjPanel.setBackground(Color.green);
        westjPanel.setPreferredSize(new Dimension(150, 600));
        jf.add(westjPanel, BorderLayout.WEST);

        // 在线用户查询按钮
        JButton search = new JButton("查找在线列表");
        westjPanel.add(search);
        
        // 在线用户列表(滚动面板)
        JScrollPane listScroll = new JScrollPane(onlineUserList);
        listScroll.setPreferredSize(new Dimension(90, 500));
        westjPanel.add(listScroll);

        // 消息展示区域
        jTextArea = new JTextArea();
        jTextArea.setFont(new Font("楷体", Font.BOLD, 24));
        jTextArea.setPreferredSize(new Dimension(400, 450));
        jTextArea.setEditable(false);
        centerjPanel.add(jTextArea);

        // 消息输入区域
        JLabel jLabel = new JLabel("消息栏:");
        centerjPanel.add(jLabel);
        JTextField jTextField = new JTextField();
        jTextField.setPreferredSize(new Dimension(200, 100));
        centerjPanel.add(jTextField);

        // 发送按钮
        JButton button = new JButton("发送");
        centerjPanel.add(button);

        // 后台线程:监听服务端消息
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        String mes = mClient.readmes();
                        // 处理在线用户列表响应
                        if (isSearching.get()) {
                            String[] idlist=mes.split(",");
                            for(String s:idlist) {
                                if(!s.equals(idname)&&!s.equals("")) {
                                    SwingUtilities.invokeLater(() -> onlineUserModel.addElement(s));
                                }
                            }
                            isSearching.set(false);
                        } else {
                            // 处理聊天消息
                            SwingUtilities.invokeLater(() -> {
                                jTextArea.append(idname+"收到:" + mes + "\n");
                                jTextArea.setCaretPosition(jTextArea.getText().length());
                            });
                        }
                    } catch (Exception e) {
                        SwingUtilities.invokeLater(() -> {
                            jTextArea.append("连接异常:" + e.getMessage() + "\n");
                        });
                        e.printStackTrace();
                        break;
                    }
                }
            }
        }).start();

        // 在线用户查询按钮事件
        search.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (isSearching.get()) {
                    JOptionPane.showMessageDialog(jf, "正在查询在线列表,请稍等");
                    return;
                }
                isSearching.set(true);
                onlineUserModel.clear();
                try {
                    mClient.writemes("6:");
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            }
        });

        // 发送按钮事件(私聊/群发切换)
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String mes = jTextField.getText().trim();
                if(mes.isEmpty()) return;
                String target=onlineUserList.getSelectedValue();
                try {
                    if(target==null) {
                        // 群发
                        mClient.writemes("4:" + mes);
                        SwingUtilities.invokeLater(() -> {
                            jTextArea.append(idname + "群发:" + mes + "\n");
                            jTextArea.setCaretPosition(jTextArea.getText().length());
                            jTextField.setText("");
                        });
                    } else {
                        // 私聊
                        mClient.writemes("3:"+target+","+mes);
                        SwingUtilities.invokeLater(() -> {
                            jTextArea.append(idname+"私聊:"+mes+"\n");
                            jTextArea.setCaretPosition(jTextArea.getText().length());
                            jTextField.setText("");
                        });
                    }
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
        jf.setVisible(true);
    }
}

三、运行步骤

  1. 启动服务端:运行Msever.java的 main 方法,控制台输出 "启动服务器,监听 9999 端口..."
  2. 启动客户端:运行GameUI.java的 main 方法,弹出登录界面
  3. 注册账号:输入账号、密码、用户名,点击 "注册",提示 "注册成功"
  4. 登录:输入注册的账号、密码,点击 "登录",提示 "登录成功" 后弹出聊天界面
  5. 多客户端测试:重复步骤 2-4,启动多个客户端,实现私聊 / 群发 / 在线查询

四、核心知识点总结

  1. Socket 通信:ServerSocket 监听端口,Socket 建立客户端 - 服务端连接,通过 IO 流收发数据
  2. 多线程并发 :每个客户端对应一个线程,避免单线程阻塞;共享集合加synchronized保证线程安全
  3. Swing UI 编程 :EDT 线程更新组件,避免 UI 卡顿;使用DefaultListModel动态更新列表
  4. 协议设计 :自定义指令格式(指令码:参数),实现客户端和服务端的标准化交互

五、优化方向

  1. 密码加密:当前密码明文传输,可使用 MD5/SHA 加密
  2. 断线重连:客户端检测连接断开后自动重连
  3. 历史消息:增加文件 / 数据库存储聊天记录
  4. 异常处理:完善 try-catch,避免单个客户端异常导致服务端崩溃
  5. UI 美化:使用更现代的 UI 框架(如 JavaFX)替代 Swing

本教程完整实现了一个基础的多人聊天室系统,覆盖了 Java Socket、多线程、Swing 核心知识点,适合入门学习和二次开发。如果有问题,欢迎在评论区交流~

相关推荐
Re.不晚15 小时前
JAVA进阶之路——数据结构之线性表(顺序表、链表)
java·数据结构·链表
毅炼15 小时前
Java 基础常见问题总结(3)
java·开发语言
m0_7482299915 小时前
PHP简易聊天室开发指南
开发语言·php
码云数智-大飞15 小时前
从回调地狱到Promise:JavaScript异步编程的演进之路
开发语言·javascript·ecmascript
froginwe1115 小时前
jQuery 隐藏/显示
开发语言
一晌小贪欢15 小时前
深入理解 Python HTTP 请求:从基础到高级实战指南
开发语言·网络·python·网络协议·http
Cinema KI15 小时前
C++11(下) 入门三部曲终章(基础篇):夯实语法,解锁基础编程能力
开发语言·c++
m0_7482299915 小时前
PHP+Vue打造实时聊天室
开发语言·vue.js·php
亓才孓15 小时前
[JDBC]事务
java·开发语言·数据库