select()或poll()无法及时发现TCP异常断开,因TCP无内置心跳,内核仅在send()/recv()时触发错误,而I/O多路复用仍报告就绪;须结合应用层心跳与超时检测主动判定断连并回滚状态机。

为什么 select() 或 poll() 无法及时发现 TCP 连接异常断开
本地 TCP socket 处于 ESTABLISHED 状态时,对端静默掉线(如拔网线、进程崩溃未发 FIN),本端不会立刻收到通知。操作系统内核只在尝试 send() 或 recv() 时才会触发错误(如 ECONNRESET、ETIMEDOUT),而 select()/poll() 仍会持续报告可读/可写——因为接收缓冲区可能还有残留数据,发送缓冲区也尚未填满。
这意味着:状态机若依赖「等待 I/O 就绪」来推进,很可能在连接已失效数秒甚至数十秒后,才因一次 send() 失败而感知到断连,回滚严重滞后。
- 典型现象:
poll()返回POLLIN,但后续recv()返回 0(正常关闭)或 -1 +errno == ECONNRESET;更糟的是,它可能根本没返回,你还在等下一条消息 - 根本原因:TCP 是面向连接的流协议,不内置心跳;保活机制(
TCP_KEEPALIVE)默认开启慢(2小时)、不可控、且部分中间设备会拦截 keepalive 包 - 结论:不能靠系统级 I/O 多路复用被动等待断连信号,必须主动探测
如何用应用层心跳 + 超时检测强制触发状态机回滚
核心思路:在应用层定义明确的「连接活跃」语义,并用定时器驱动验证。一旦连续 N 次心跳无响应,立即判定连接异常,调用状态机的回滚入口函数(如 state_machine.rollback())。
- 心跳包格式要轻量,例如仅 4 字节:固定 magic(
0x48454152)+ 序号,避免序列化开销 - 发送侧用
std::chrono::steady_clock记录每次心跳发送时间,接收侧收到后立即回复 ACK(同样轻量),并更新最后通信时间戳 - 独立于网络线程的监控线程(或主事件循环中的定时回调)每 500ms 检查:
now - last_heard_time > 2000ms→ 触发on_connection_lost() - 关键细节:检查必须用单调时钟(
steady_clock),禁用system_clock;超时阈值需大于心跳周期 × 2,留出网络抖动余量
setsockopt(SO_KEEPALIVE) 能不能直接用?
可以启用,但不能替代应用层心跳。
立即学习“C++免费学习笔记(深入)”;
- Linux 默认 keepalive 参数:空闲 7200s 后开始探测,间隔 75s,最多 9 次失败 → 全程近 2.5 小时才断连,完全不可接受
- 虽可用
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle))等调整,但 Windows/macOS 行为不一致,且某些防火墙/NAT 设备会丢弃 keepalive 探测包 - 实测中,设为 10s idle / 3s interval / 3 probes(即 19s 断连),在公网环境仍常被干扰失效;而应用层心跳走业务端口、带业务上下文,穿透性更强
- 建议:开启
SO_KEEPALIVE作为兜底(防程序卡死无心跳),但状态机回滚逻辑必须绑定应用层心跳超时
状态机回滚时要注意哪些资源泄漏点
回滚不是简单跳回初始状态,而是要清理与该连接强绑定的运行时资源,否则下次重连可能复用脏状态。
- 释放所有 pending 的异步操作:比如正在
async_read()的boost::asio::streambuf缓冲区、未完成的std::promise实例,必须显式cancel()或set_exception() - 清除定时器句柄:如果心跳、重试、超时都用了
std::thread+std::condition_variable或asio::steady_timer,回滚前需cancel()并wait()或reset(),避免 timer 回调访问已析构对象 - 避免双重回滚:在
on_connection_lost()中加if (state_ != STATE_DISCONNECTED)保护,防止网络线程和心跳监控线程同时触发 - 最易忽略:连接 ID 映射表(如
std::unordered_map<int statemachine></int>)里对应的条目必须 erase,否则内存泄漏且新连接复用 fd 时可能误查旧状态
回滚的边界很关键——它不是恢复到某个抽象“安全点”,而是让整个连接上下文回归到可重建、可销毁、不污染其他连接的状态。这个动作本身必须是幂等且快速的,不能含任何阻塞 I/O 或锁竞争。


















