必须用SendAsync。同步Send在连接异常时会阻塞线程,异步可避免阻塞并支持超时控制;心跳应答需超时重试+连续失败计数,TCP.Connected不可靠;KeepAlive不能替代应用层心跳;心跳与业务数据须协议分离。

心跳包该用 Send 还是 SendAsync?
必须用 SendAsync。同步 Send 在连接已断但 TCP 状态未及时回收(如对方突然掉电、NAT 超时)时会卡住线程,甚至阻塞整个心跳定时器。异步发送能避免阻塞,也方便配合 CancellationToken 控制超时。
常见错误现象:心跳线程卡死、CPU 占用突增、后续所有消息无法发出但日志里没报错。
- 心跳数据尽量小,推荐固定 1 字节(如
0xFF),避免触发 Nagle 算法延迟 - 务必设置
Socket.SendTimeout = 3000,并捕获SocketException中的ErrorCode == 10054(连接被远程重置)或10053(软件导致连接中止) - 不要在
SendAsync回调里直接重发——需先检查args.SocketError == SocketError.Success
怎么判断“连接还活着”而不是“只是没回包”?
单靠发包不等于连通。真实可用的心跳必须有应答 + 超时重试 + 连续失败计数。TCP 层的 Connected 属性几乎没用——它只反映上一次操作是否成功,不是实时状态。
使用场景:移动网络切换、WiFi 断连再重连、防火墙静默丢弃 ACK。
- 客户端发心跳后,启动独立
Task.Delay(5000, token)等待服务端回包;超时即视为异常 - 连续 3 次心跳无响应(非超时,是根本没收到任何数据),才触发断连逻辑
- 服务端收到心跳后必须立刻
Send一个确认字节(如0x00),不能攒着或延迟回复 - 禁用
Socket.NoDelay = false(即开启 Nagle),否则小包会被缓冲,心跳响应延迟不可控
KeepAlive 选项能不能代替应用层心跳?
不能。系统级 KeepAlive(通过 SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true) 启用)只探测链路层是否可达,且默认间隔长达 2 小时,对业务级连接保活毫无意义。
性能影响:开启系统 KeepAlive 会增加少量内核开销,但远不如应用层心跳可控;兼容性上,某些嵌入式设备或旧版 Windows 对 KeepAlive 响应不规范,容易误判。
- 若仍要启用,务必手动设置参数:
socket.IOControl(IOControlCode.KeepAliveValues, keepAliveBytes, null),其中keepAliveBytes是 12 字节结构体,把间隔压到 30 秒以内 - 即便设了 KeepAlive,仍需应用层心跳——前者防物理断连,后者防中间设备(如代理、负载均衡)静默超时
- Linux 下
/proc/sys/net/ipv4/tcp_keepalive_time会影响全局行为,但 .NET 不保证跨平台一致,别依赖
心跳和业务数据共用一个 Socket,如何避免互相干扰?
必须分离协议帧。心跳包不是“额外流量”,而是协议的一部分,得让收发两端能明确区分:这是心跳,不是业务指令。
容易踩的坑:服务端收到心跳字节后当成普通消息解析,抛出 InvalidDataException;或客户端把业务响应误认为心跳应答,重置心跳计数器。
- 最简方案:约定心跳为单字节,如客户端发
0xFF,服务端只接收并返回0x00,其他所有数据都走带长度头的业务帧(如 4 字节 int32 表示后续字节数) - 读取时用
Socket.Receive的SocketFlags.Peek先窥探第一个字节,再决定是丢弃处理还是进业务管道 - 心跳超时检测逻辑必须独立于业务接收循环——比如用
Timer或PeriodicTimer触发,而非在每次ReceiveAsync后顺手检查
真正难的不是发包,是让心跳逻辑和业务解耦又不漏判。很多人卡在“为什么明明连着却总断连”,其实问题不在心跳本身,而在没守住这个边界:心跳只管通道存活,业务只管语义正确,两者时间尺度、错误策略、重试逻辑全得分开算。











