socket.accept()会阻塞主线程,必须异步处理;每个客户端需独立socket和缓冲区;send/receive需检查返回值并处理粘包;须设keepalive、捕获各类异常、显式关闭连接。

Socket.Accept() 会阻塞主线程,必须用异步或新线程
直接在主线程里写 socket.Accept(),整个程序就卡死了——这不是 bug,是 TCP Socket 的默认行为。Accept 等待客户端连接时,线程会挂起,UI 冻住、定时器停摆、后续逻辑全停。
常见错误现象:Console.ReadLine() 能输,但新客户端连不上;WinForm 界面无响应;服务器看起来“启动了”,却收不到任何消息。
- 推荐用
BeginAccept()+ 回调,或 .NET 4.5+ 的AcceptAsync()(需预分配SocketAsyncEventArgs) - 简单验证可用
Task.Run(() => { var client = serverSocket.Accept(); ... }),但仅限学习,别用于生产——线程池耗尽风险高 - 千万别在 UI 线程(如 WinForm 的
Button.Click里)同步 Accept
每个客户端必须单独维护 Socket 和缓冲区,不能共用
很多人把所有客户端的读写都往一个 byte[] 缓冲区上怼,结果消息串包、乱码、崩溃。Socket 是有状态的:每个连接对应独立的内核句柄、接收窗口、发送队列。
使用场景:你得为每个 Accept() 返回的 client socket 分配专属缓冲区和状态对象,比如封装成 ClientSession 类。
- 错误做法:
static byte[] buffer = new byte[1024];全局复用 → 多个 client 同时读写,数据覆盖 - 正确做法:每次 Accept 后 new 一个
byte[1024],或用ArrayPool<byte>.Shared.Rent(1024)</byte>降低 GC 压力 - 记得在 client 断开时
clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close();,否则端口占用、内存泄漏
Send() 不保证一次发完,Recv() 不保证一次收全
Socket.Send() 返回值是“实际发出字节数”,可能小于你传入的长度;Socket.Receive() 同理,TCP 是流式协议,不是报文协议。没处理好就出现“只收到半条消息”或“粘包”。
性能影响:盲目循环 Send/Receive 会吃 CPU;用错方式重试还会触发 Nagle 算法延迟。
- 必须检查返回值:
int sent = client.Send(data); if (sent != data.Length) { /* 补发剩余部分 */ } - 接收端建议加简单协议头,比如前 4 字节存消息长度(
BitConverter.GetBytes(len)),再按长度分次 Receive - 别用
client.Receive(buffer) == 0当断连判断——它只表示对端关闭连接,网络中断时可能永远不返回 0
心跳 + 异常捕获才是真实环境的底线
本地测试时一切正常,一上公网或过防火墙,客户端悄无声息掉线,服务端 socket 还挂着,内存越涨越高。TCP 不会主动通知你“对方死机了”或“WiFi 断了”。
容易被忽略的地方:很多人只 try-catch SocketException,却漏了 ObjectDisposedException(socket 已 Close 后又被读写)和超时引发的 IOException。
- 给每个 client socket 设置
client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true),并配IOControl(IOControlCode.KeepAliveValues, ...)控制间隔 - 接收循环里必须包 try/catch,且 catch 后要显式
client.Close()并从管理列表中移除 - 别依赖
Connected属性——它只反映上次操作状态,不是实时连接检测
真实聊天室最难的不是收发消息,是让连接“可感知、可回收、不泄漏”。协议可以简陋,但资源生命周期必须闭环。











