直接用System.in读取客户端输入会卡住服务端,因其为阻塞式同步IO,需为每个Socket分配独立线程处理I/O,避免单连接阻塞全局;readLine()依赖换行符,客户端须发送\n并flush;共享用户列表应使用CopyOnWriteArrayList等线程安全结构;关闭资源须先flush并关闭包装流,再关Socket,并设置超时或心跳应对闪断。

为什么直接用 System.in 读取客户端输入会卡住整个服务端
因为 System.in.read() 是阻塞式同步 IO,一旦某个客户端没发消息,线程就停在那儿,其他客户端的读写全被拖住。多客户端场景下,必须把每个连接的输入流单独交给独立线程处理,不能让一个慢连接拖垮全局。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 为每个
Socket连接新建一个线程(或丢进线程池),专门调用该连接的InputStream的read()或包装成BufferedReader.readLine() - 别在主线程里循环调用
serverSocket.accept()后立刻读数据——接受连接和处理消息要拆开 - 注意:Java 8+ 推荐用
ExecutorService管理线程,避免无限制创建Thread导致 OOM
BufferedReader.readLine() 为啥有时读不到换行就一直挂起
它等的是完整的行(即遇到 \n、\r\n 或 \r 才返回),而很多终端(比如 Windows 的 cmd)默认用 \r\n,但 telnet 客户端可能只发 \n;如果客户端没发换行符,readLine() 就永远不返回。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 客户端发送消息后务必加
\n(例如out.write("hi\n".getBytes()); out.flush();) - 服务端别依赖
readLine()做协议解析——它不是为聊天设计的,只是“碰巧能用”;真要健壮,得自己按字节读、检测分隔符 - 调试时可用
telnet localhost 8080测试,输完按回车(自动带\r\n),比手写 socket 客户端更直观
多个客户端怎么共享同一份在线用户列表和广播逻辑
不能每个线程都维护自己的 List<Socket>,否则 A 客户端上线,B 客户端根本不知道。必须用线程安全的共享结构,且广播时要跳过发送者自己。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 用
Collections.synchronizedList(new ArrayList<>())或更推荐CopyOnWriteArrayList存储活跃的PrintWriter(封装了每个客户端的输出流) - 每次新连接建立后,把它的
PrintWriter加入共享列表;断开时记得从列表中移除(放在try-finally或try-with-resources里) - 广播消息时遍历列表,对每个
pw调用pw.println(msg); pw.flush();,但要先判断是否是当前发送者的pw(可提前存个标识,比如用Map<Socket, String username>)
关闭连接时 Socket、InputStream、OutputStream 到底谁先关
顺序错了会导致远端收不到 EOF、对方线程卡死、资源泄漏。常见错误是只关了 Socket 却忘了 flush 输出流,或者先关了输出流再往里面写,抛 IOException。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 标准顺序:先
flush()和close()所有包装流(BufferedReader、PrintWriter),最后关Socket - 别依赖
Socket.close()自动关底层流——JDK 行为不保证,尤其在异常路径下容易漏 - 所有流和 socket 都得放在
finally块里关,或用try-with-resources(注意:只有实现了AutoCloseable的才能用,Socket可以,但要确保构造时不抛异常)
最麻烦的其实是网络闪断:客户端突然关机或断网,服务端可能很久才感知到 read() 返回 -1 或抛异常。这时候得靠心跳或设置 socket.setSoTimeout(30000) 主动超时,不然空闲连接会一直占着线程和内存。


















