初学者用TcpClient/TcpListener,高并发必须用裸Socket配AcceptAsync;TcpClient.AcceptTcpClient()因同步阻塞、无取消机制、无法设选项、GC压力大而生产卡死;SocketAsyncEventArgs需复用并重置;ReadAsync返回0即断连,非无数据;KeepAlive等选项须连接前设置;Shutdown后才Close,避免丢数据。

直接说结论:初学者或局域网轻量通信,用 TcpClient 和 TcpListener;需要高并发、长连接、精细控制(如心跳、超时、缓冲区预判),必须用裸 Socket 配合 AcceptAsync 和 SocketAsyncEventArgs。
为什么 TcpClient.AcceptTcpClient() 一上生产就卡死
它本质是同步阻塞调用,每次只接受一个连接,且不提供取消机制。你写个 while 循环反复调用它,主线程就会在 AcceptTcpClient() 处挂住,新连接排队等前一个处理完——这不是“并发”,是“串行排队”。
- 客户端连上来后,服务端无法立即响应下一个连接请求,延迟陡增
- 没异常兜底:网络抖动或监听器被提前
Stop(),AcceptTcpClient()会抛ObjectDisposedException,整个服务崩溃 - 无法设置
SO_KEEPALIVE、SO_LINGER等关键选项,断连检测靠猜 - 不能复用
Socket实例,每 Accept 一次就 new 一个TcpClient,GC 压力大,句柄易泄漏
Socket.AcceptAsync() 怎么避免 InvalidOperationException
错误常发生在重复 new SocketAsyncEventArgs 或未重置状态。这个对象不是一次性用品,必须复用、手动清空缓冲区、重置事件完成标志。
- 全局缓存一个
SocketAsyncEventArgs实例(或对象池),不要每次 Accept 都 new - 每次调用
AcceptAsync()前,必须调用args.SetBuffer(null, 0)清除旧缓冲区绑定 - 在回调
Completed事件里,先检查args.SocketError == SocketError.Success,再取args.AcceptSocket - 拿到
AcceptSocket后立刻设选项:acceptSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true) - 别忘了调用
acceptSocket.ReceiveAsync(args)启动首次读取,否则连接建立后收不到数据
NetworkStream.ReadAsync() 返回 0 就该停,不是继续等
TCP 是字节流,ReadAsync 返回 0 表示对端已关闭连接(FIN 包到达),不是“暂时没数据”。继续 Read 会永远挂起或抛异常。
- 每次
ReadAsync后必须检查返回值:if (n == 0) break; - 不要把
NetworkStream当成可重复使用的“管道”——一次连接对应一个生命周期,读到 0 就该释放TcpClient或Socket - 如果用裸
Socket.Receive(),同样要检查返回值;0表示对方关闭,负数(如-1)通常意味着错误,需查SocketError - 别依赖
client.Client.Connected判断是否还活着——Windows 下 NAT 断连后这个属性可能仍为true,唯一可靠的是ReadAsync返回 0 或抛IOException
TcpClient.Client 属性什么时候能用
只能在 TcpClient 构造之后、Connect() 之前访问 Client 属性。一旦连接建立,再访问会抛 InvalidOperationException。
- 想设 KeepAlive?必须在
tcpClient.Connect()前写:tcpClient.Client.SetSocketOption(...) - 连接中要改选项(比如动态调
LingerTime),只能用裸Socket,TcpClient不暴露运行时修改入口 - 别试图通过反射绕过限制——
Client内部字段在 .NET Core+ 已被封装更严,反射失败率高且不可靠 - 若只是发心跳包,用
NetworkStream.WriteAsync()主动写点数据比依赖系统 KeepAlive 更可控
最易被忽略的点:所有基于 Socket 的代码,Shutdown 和 Close 的顺序不能颠倒。先 Shutdown(SocketShutdown.Both),再 Close(),否则可能丢最后一段数据或触发 RST 而非 FIN。



















