tcpclient自带keepalive基本没用,因其默认2小时超时、不可控且无法感知应用层断连;可靠心跳需应用层主动发包+超时判断,并配合ack响应、异步io、指数退避重连及状态隔离等机制。

为什么 TcpClient 自带的 KeepAlive 基本没用
Windows 默认开启的 TCP KeepAlive 是内核层机制,超时时间长(默认 2 小时)、不可控、且无法感知应用层断连(比如对方进程崩溃但网卡还通)。实际业务中,你看到连接“挂着”却收不到数据,就是因为这个 KeepAlive 没触发或太晚触发。
真正可靠的心跳必须由应用层主动发包 + 超时判断。关键点在于:不能只依赖 Socket.Connected —— 它只是缓存值,不发起探测,断连后仍可能返回 true。
- 务必在发送/接收路径外,单独起一个定时器(如
System.Threading.Timer)定期发心跳 - 心跳包内容建议用固定长度短字节(如
new byte[] { 0xFF }),避免序列化开销 - 接收端收到心跳后必须回一个 ACK(哪怕只是原样 echo),否则单向网络故障无法发现
怎么用 Socket.SendAsync 和 Socket.ReceiveAsync 配合心跳计时器
同步阻塞调用(Send/Receive)会卡死心跳逻辑;异步模式才能让发送、接收、心跳探测三者并行不冲突。
核心结构是:一个 Timer 每 30 秒触发一次心跳发送 → 发送后启动一个 CancellationTokenSource 设 10 秒超时 → 若超时前没收到 ACK,就标记连接异常。
- 心跳发送不要用
SendAsync后立刻await,否则会阻塞定时器线程;应 fire-and-forget,靠回调或Task状态判断结果 -
ReceiveAsync的SocketAsyncEventArgs必须复用,不能每次 new,否则 GC 压力大 - 心跳超时 ≠ 立即断连:先尝试发一次 “探活” 包(如再发一次心跳),失败后再走重连流程
重连时为什么不能直接 new TcpClient() 就连
盲目重试会导致 TIME_WAIT 占满端口、DNS 缓存未更新、服务端限流等隐性问题。真实场景下,重连必须带退避策略和状态隔离。
- 首次失败后等待 1 秒,第二次失败等 2 秒,第三次等 4 秒(指数退避),最大不超过 30 秒
- 每次重连前检查
Dns.GetHostAddressesAsync是否变化,防止服务端 IP 切换后连错机器 - 重连过程中禁止新业务消息入队,避免重连成功后积压消息洪峰打崩服务端
- 若连续 5 次重连失败,应降级为离线模式(如本地缓存写入 + 后台轮询),而不是无限循环
心跳检测里最容易被忽略的三个细节
很多实现跑几天就出问题,往往栽在这几个点上:
-
Socket关闭后,关联的Timer必须Dispose(),否则内存泄漏 + 定时器还在偷偷触发回调 - 心跳包的发送和 ACK 接收必须共享同一个序号或时间戳(比如用
Environment.TickCount),否则无法区分是哪次心跳的响应 - 多线程环境下,心跳超时标记(如
isHeartbeatTimeout)必须用Volatile.Write或Interlocked,否则其他线程读到脏值
心跳不是加个定时器发个包就完事,它本质是客户端和服务端共同维护的一套轻量级会话状态机。只要有一端没严格按约定响应,整条链路的可靠性就会坍塌。











