socket.connected不可靠,因它仅返回内核缓存状态,不探测网络实际连通性;必须用非阻塞零字节send配合超时与错误码判断,并辅以指数退避重连和资源生命周期管理。

不能只靠 Socket.Connected 判断连接是否有效,它只是缓存值,断连后仍可能返回 true;真正可靠的心跳必须由应用层主动发包 + 超时响应判断。
为什么 Socket.Connected 不可信
这个属性不发起任何网络探测,只是返回内核上次收发操作后缓存的连接状态。比如对端进程崩溃、网线被拔、防火墙静默丢包,Socket.Connected 都可能维持 true 几分钟甚至更久。Windows 默认的 TCP KeepAlive 超时是 2 小时,业务根本等不起。
常见错误现象:
- 客户端显示“已连接”,但发不出消息,服务端收不到任何数据
- 调用
Send时阻塞或抛出WSAENOTCONN,但之前Connected还是true
正确做法是:在业务逻辑外,单独起一个定时器(如 System.Threading.Timer),每 30 秒左右触发一次心跳探测。
怎么用零字节 Send 真实探测连接状态
这是微软文档明确推荐的轻量级探测方式,比 Poll 更可靠(Poll 对单向断连不敏感)。
关键点:
- 必须设为非阻塞模式:
socket.Blocking = false - 发送空数据:
socket.Send(new byte[1], 0, 0)—— 注意长度为 0,不是null或未初始化数组 - 捕获
SocketException,只把NativeErrorCode == 10035(WSAEWOULDBLOCK)当作“仍连通” - 其他错误码(如
10054、10053)或超时,一律视为断连
示例片段:
bool IsAlive(Socket s)
{
bool oldState = s.Blocking;
try
{
s.Blocking = false;
s.Send(new byte[1], 0, 0); // 零字节探测
return true;
}
catch (SocketException ex)
{
return ex.NativeErrorCode == 10035; // WSAEWOULDBLOCK 表示连接还在
}
finally
{
s.Blocking = oldState;
}
}
心跳 + 断线重连必须带退避与状态隔离
盲目重连会快速耗尽本地端口(TIME_WAIT)、触发服务端限流、或连到已下线的旧 IP(DNS 缓存未刷新)。
实操建议:
- 首次失败后等待 1 秒,第二次 2 秒,第三次 4 秒……上限设为 30 秒(指数退避)
- 每次重连前调用
Dns.GetHostAddressesAsync(host)检查 IP 是否变化,避免连错机器 - 重连过程中暂停业务消息入队(例如清空待发队列或切换到本地缓存写入)
- 连续 5 次失败后,转入离线模式,不再自动重试,改为后台低频轮询
特别注意:重连成功后,不要直接恢复全量消息推送 —— 先发一个探活请求,确认服务端通道就绪再放行积压消息,否则容易打崩对方。
异步模式下 Timer 和 SocketAsyncEventArgs 的生命周期管理
用 SendAsync/ReceiveAsync 时,最容易忽略的是资源泄漏和状态错乱。
必须做到:
-
Timer必须在 Socket 关闭时显式Dispose(),否则回调持续触发,引发 NullReferenceException 或重复发包 -
SocketAsyncEventArgs实例要复用,不能每次 new —— 否则 GC 压力陡增,尤其在高并发长连接场景 - 心跳发送不能
await SendAsync(...),应 fire-and-forget,靠Completed事件或SocketError字段判断结果 - 收到心跳包后,服务端必须回 ACK(哪怕原样 echo),否则单向断连无法发现
最常被忽略的细节:心跳定时器启动后,没人负责在 Socket.Close() 或异常退出路径中调用 timer.Dispose() —— 这个泄漏会悄无声息地拖垮服务数天后才暴露。











