TcpClient.Connected不可信,因其仅反映上次I/O操作时的状态,无法实时检测NAT超时、静默断连等异常;必须结合Poll+Available主动探测或发送探测包验证连接有效性。

TcpClient 不是“开箱即用”的黑盒,不处理粘包、不自动重连、不感知静默断开——直接 new TcpClient() 然后 Read/Write,十有八九在线上跑几天就掉连接或卡死。
为什么 TcpClient.Connected 属性根本不可信
TcpClient.Connected 只反映上一次 Send 或 Receive 操作时的连接状态,不是实时心跳检测。NAT 超时、防火墙静默丢包、服务端进程崩溃但未发 FIN,都会让这个属性继续返回 true,而后续 ReadAsync 会一直挂起或突然抛异常。
- 真实场景中,必须配合
Poll+Available做主动探测:先client.Client.Poll(100, SelectMode.SelectRead),再检查client.Client.Available == 0—— 两者同时成立才大概率已断开 - 别在发送前检查
Connected:它对即将发生的 Send 没预测能力,反而给你虚假安全感 - Windows 上
Connected在断连后可能延迟数秒才变false,Linux 更慢;依赖它做重连判断会导致连接堆积
NetworkStream.ReadAsync 返回 0 就必须立即释放资源
这是 TCP 协议层明确规定的 EOF 信号:对方已调用 Shutdown(SD_SEND) 或关闭连接。但很多代码把它当成“暂时没数据”继续循环等待,结果线程卡死、内存泄漏、句柄耗尽。
-
ReadAsync返回值为0时,必须跳出读循环,调用client.Close()或Dispose(),不能复用该TcpClient实例 - 不要用
while (stream.DataAvailable)判断——DataAvailable是同步非阻塞查询,和异步读取逻辑冲突,且 Windows 下不准 - 如果业务协议是固定包头(如 4 字节长度),读到不足包头长度就应视为异常断开,而非等待
异步写入必须检查 WriteAsync 的返回值
WriteAsync 是“尽力而为”,不保证全部字节发出。尤其在高负载或网络拥塞时,它可能只写入部分缓冲区,返回值小于 buffer.Length。忽略这点会导致协议解析错位、数据截断。
- 正确做法是用循环确保写完:
int total = 0; while (total - 别把
WriteAsync当作 fire-and-forget:未 await 或未检查返回值,等于放弃数据可靠性保障 -
SendTimeout对异步写无效,它只影响同步Send;超时控制得靠CancellationToken配合WriteAsync
别在 while 循环里反复 new TcpClient
频繁创建销毁 TcpClient 实例会快速耗尽本地端口(TIME_WAIT 状态)、触发 GC 压力,且 DNS 解析、三次握手、TLS 握手(如有)全得重来,延迟陡增。
- 连接池化更合理:维护一个
ConcurrentDictionary<string tcpclient></string>,Key 为"{ip}:{port}",按需复用;注意加锁或原子操作避免并发 Close - 重连时优先尝试复用已有实例(先
Poll探测),失败再新建;指数退避间隔至少从 300ms 起步,避免重连风暴 - 所有
TcpClient必须显式Dispose()或用using,否则底层Socket句柄不会释放,Windows 上最多 65535 个连接就卡死
最常被跳过的一步:TcpClient 关闭后,NetworkStream 不会自动失效,仍可能缓存未刷出的数据;必须在 Close() 前确保 WriteAsync 已完成,或调用 stream.FlushAsync()(仅对启用了 WriteBuffering 的情况有效)。


















