必须用新线程处理每个accept()返回的socket,避免阻塞主线程;用concurrentlinkedqueue作消息队列,record封装消息;设socket超时和utf-8编码,显式flush,加强连接管理。
用 ServerSocket 启动聊天服务端,但别让 accept() 阻塞主线程
服务端必须持续监听新客户端连接,但 ServerSocket.accept() 是阻塞调用——如果直接写在主线程里,后续逻辑(比如广播消息、管理用户列表)就卡死了。必须把每个新连接丢进独立线程处理。
- 每次
accept()返回一个Socket,立刻用new Thread(() -> handleClient(socket)).start()启动处理逻辑 - 不要复用线程池来处理连接建立阶段;连接刚建立时还不知道客户端身份,线程边界要清晰
- 注意:JDK 21+ 可用虚拟线程(
Thread.ofVirtual().start()),但传统平台仍以new Thread最直观可靠
多个客户端共用一个共享消息队列,ConcurrentLinkedQueue 比 synchronized 块更轻量
所有客户端发来的消息需广播给其他人,得有个线程安全的中转容器。用 synchronized(this) 包裹 ArrayList 写入/遍历,会在高并发下形成明显锁争用;而 ConcurrentLinkedQueue 无锁、适合“一生产多消费”场景。
- 消息对象建议封装为不可变类(如
record ChatMessage(String sender, String text, long timestamp)) - 广播时遍历在线
Socket列表,逐个写入PrintWriter;若某客户端断连,write()或flush()抛IOException,需捕获并清理该连接 - 别用
CopyOnWriteArrayList存储在线用户——它适合读多写少,但聊天室里上线/下线频繁,写操作开销太大
BufferedReader.readLine() 阻塞读取时,客户端断连会导致线程永久挂起
每个客户端连接对应一个读线程,典型写法是 while ((line = reader.readLine()) != null)。但 TCP 连接异常中断(比如网线拔掉、手机切飞行模式)时,readLine() 不会立即返回 null 或抛异常,可能卡住几十秒甚至更久。