必须用新线程处理每个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 或抛异常,可能卡住几十秒甚至更久。
- 必须给 socket 设置超时:
socket.setSoTimeout(30_000),这样readLine()在无数据时抛SocketTimeoutException,可据此主动关闭连接 - 同时启用
socket.setKeepAlive(true),让底层探测死链(但 keepalive 默认间隔长,不能替代应用层心跳) - 更健壮的做法是客户端定期发
PING,服务端用单独定时任务检查各连接最后心跳时间,超时即踢出
客户端发送中文消息乱码?重点查 InputStreamReader 的字符集和 PrintWriter 的自动刷新
服务端用 new InputStreamReader(socket.getInputStream(), "UTF-8") 读,客户端却用默认平台编码(Windows 是 GBK)发,必然乱码;另外,PrintWriter 若没开自动刷新,消息会滞留在缓冲区不下发。
立即学习“Java免费学习笔记(深入)”;
- 服务端读取:明确指定
"UTF-8",不要依赖Charset.defaultCharset() - 服务端写出:用
new PrintWriter(socket.getOutputStream(), true)——第二个参数true表示自动flush - 客户端同理:
OutputStreamWriter和PrintWriter都要显式设"UTF-8",且写完调flush()(或构造时设自动刷新)










