在Java里如何实现简易聊天室服务器_Java网络通信项目说明

ServerSocket可实现简易TCP聊天室:服务端用accept()阻塞监听,每客户端分配ClientHandler线程,广播消息需线程安全集合存储PrintWriter并及时移除断开连接;客户端需双线程收发,注意UTF-8编码与资源关闭。

用 ServerSocket 实现基础 TCP 聊天室服务器

Java 原生 ServerSocket 就够用,不需要 Spring Boot 或 Netty——只要支持多客户端连接、广播消息,就能跑通简易聊天室。核心是“一个服务端线程监听连接,每个客户端分配一个独立线程处理读写”,适合学习网络模型,也足够应付几十人小规模测试。

注意:这不是生产级方案(无心跳、无断线重连、无消息序列化),但能清晰看到 SocketInputStreamOutputStream 如何协作。

  • 服务端启动后,调用 serverSocket.accept() 阻塞等待连接,每接受一个 Socket 就新建一个 ClientHandler 线程
  • 所有已连接的 Socket 输出流需存入线程安全集合(如 Collections.synchronizedList(new ArrayList())
  • 广播时遍历该集合,对每个 PrintWriter 调用 println()

    flush()
    ,否则客户端收不到消息
  • 客户端断开时,必须从集合中移除对应 PrintWriter,否则后续广播会触发 IOException: Broken pipe
public class ChatServer {
    private static final List clients = Collections.synchronizedList(new ArrayList<>());
    
    public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("Chat server started on port 8080");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                new ClientHandler(clientSocket).start();
            }
        }
    }

    static class ClientHandler extends Thread {
        private final Socket socket;
        private final BufferedReader in;
        private final PrintWriter out;

        ClientHandler(Socket socket) throws IOException {
            this.socket = socket;
            this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            this.out = new PrintWriter(socket.getOutputStream(), true);
        }

        public void run() {
            try {
                clients.add(out);
                String msg;
                while ((msg = in.readLine()) != null) {
                    System.out.println("Received: " + msg);
                    for (PrintWriter client : clients) {
                        client.println(msg); // 广播给所有人
                    }
                }
            } catch (IOException e) {
                // 客户端异常断开,清理资源
                clients.remove(out);
            } finally {
                try { socket.close(); } catch (IOException ignored) {}
            }
        }
    }
}

客户端用 Socket 连接并收发纯文本

简易聊天室客户端只需一个 Socket、一个 Scanner(读控制台)、一个 BufferedReader(收服务端消息)。关键在于两个线程:一个负责读用户输入并发送,另一个负责监听服务端推送——不能串行阻塞。

  • 发送线程用 System.in 读取 nextLine(),避免 nextInt() 后残留换行符导致下一次读取为空
  • 接收线程用 BufferedReader.readLine() 持续监听,遇到 null 表示服务端关闭连接
  • 客户端退出前必须调用 socket.close(),否则服务端无法感知断开,clients 列表持续膨胀
  • 不要在 main 线程里直接 readLine() 后再 println(),会导致“自己发的消息自己收不到”(因为没启接收线程)

常见错误:中文乱码和连接拒绝

乱码基本是字符集不一致:InputStreamReader 默认用平台编码(Windows 是 GBK),而现代 IDE 和终端多用 UTF-8。必须显式指定:

  • 服务端构造 BufferedReader 时用 new InputStreamReader(socket.getInputStream(), "UTF-8")
  • 客户端同理,且 PrintWriter 构造需加 true 参数启用自动 flush,并指定编码:new OutputStreamWriter(socket.getOutputStream(), "UTF-8")
  • “Connection refused” 错误只说明服务端没在目标端口监听:检查 ServerSocket 是否已启动、端口是否被占用(netstat -an | grep 8080)、防火墙是否拦截
  • 启动顺序必须是先运行服务端,再启动多个客户端;反向操作必然报错

扩展性差在哪?别急着重构

这个实现撑不过 100 个并发连接——每个客户端占一个线程,线程创建/切换开销大,JVM 线程数也有上限(默认约 1024)。但对理解“连接管理”“消息分发”“资源清理”已经足够。

真正容易被忽略的是:没有区分发送者身份。当前所有消息都是“匿名广播”,如果要显示“[Alice]: hello”,就得在服务端为每个 ClientHandler 记录昵称(比如首条消息作为用户名),并在广播时拼接前缀——这需要修改 clients 存储结构,改用 Map,且首次注册时做重复校验。