应避免用TcpClient/TcpListener构建高并发服务端,因其阻塞Accept、不支持Socket选项配置、无数据预判接口、易句柄泄漏;推荐用Socket.AcceptAsync配合IOCP,并严格按协议解析字节流、显式Shutdown再Close。

别用 TcpClient 和 TcpListener 写高并发服务端——它们底层仍是 Socket,但屏蔽了关键控制点,一上生产就卡死或丢连接。
为什么 AcceptTcpClient() 会让服务端卡住
它本质是阻塞式同步 Accept,每次调用只返回一个 TcpClient,主线程会一直等客户端连上来才继续。UI 线程或控制台主线程被锁死,界面冻结、日志停更、新连接排队超时都是典型表现。
- 不是代码写错了,是模型选错了:
AcceptTcpClient()无法取消、无法复用、无法设置SO_KEEPALIVE或SO_LINGER - 你手动开线程去循环调用它,只是把阻塞转移到子线程,没解决根本问题——连接接纳吞吐量低、异常时句柄泄漏风险高
-
TcpClient.Close()不等于底层Socket.Dispose(),未读完流就关闭,容易丢包或触发ObjectDisposedException
AcceptAsync() 必须配合 SocketAsyncEventArgs 复用
直接 new SocketAsyncEventArgs 每次都用,会触发 InvalidOperationException: "The I/O operation has been aborted...",GC 压力大,且无法稳定复用缓冲区。
- 必须提前创建并复用同一个
SocketAsyncEventArgs实例(建议作为字段或池化管理) - 每次调用
AcceptAsync()前,要确保args.AcceptSocket为null,否则会抛异常 - 回调中检查
args.SocketError == SocketError.Success,成功后立即对args.AcceptSocket调用SetSocketOption()配置保活、超时等 - 处理完当前连接后,必须立刻再次调用
listener.AcceptAsync(args),否则监听就断了
Receive() 和 Send() 的字节流陷阱必须自己兜底
TCP 是字节流,不是消息队列。NetworkStream.Read() 和 Socket.Receive() 都不保证一次调用拿到完整业务消息——粘包、半包是常态,不是 bug。
- 发送端不能只调用一次
Send()就认为发完了;要循环检查返回值,直到所有字节写入内核缓冲区 - 接收端不能直接
Encoding.UTF8.GetString(buffer)整个缓冲区;buffer 里可能含残留数据或只收到半条消息 - 必须定义协议:比如头部 4 字节存 body 长度,先用
Poll()或异步循环读够头,再按长度读正文 -
Socket.Available可用于预判是否有数据可读(避免无意义Receive()),但仅限同步场景;异步收发仍需靠协议驱动
Shutdown() 和 Close() 的顺序不能颠倒
很多连接异常中断、资源泄漏、对方收不到 FIN 包,根源都在关闭逻辑错乱。
- 必须先调用
socket.Shutdown(SocketShutdown.Both),通知内核“我不会再发也不会再收”,触发 TCP 四次挥手 - 再调用
socket.Close()或Dispose()释放句柄;反过来会导致连接卡在TIME_WAIT或对方永远等不到 EOF - 如果用了
NetworkStream,要先stream.Close()(它内部会触发Shutdown),再关TcpClient;但注意TcpClient.GetStream()返回的 stream 关闭不等于 socket 关闭
真正难的不是写通第一对连接,而是让成百上千个连接在异常网络下不丢状态、不漏资源、不积压缓冲区——所有这些,都藏在 AcceptAsync() 的复用方式、Receive() 的协议解析逻辑、以及 Shutdown() 的执行时机里。


















