服务端recv在客户端异常断连时返回-1而非0的问题排查
异常返回值原因分析
本次问题中recv返回-1的核心原因是**SO_RCVTIMEO超时机制的返回规则**:
- 当
recv在设定的超时窗口内没有收到任何TCP报文(包括对端的业务数据、正常关闭的FIN包、异常断开的RST包),会直接返回-1,同时将errno设置为EAGAIN/EWOULDBLOCK,仅代表本次接收调用超时,不代表连接已经断开。 - 现有逻辑仅判断
recv的返回值为-1,没有进一步校验errno的具体值,因此把超时返回和真实的连接错误返回混为一谈。
结合客户端强杀进程的测试场景,出现连续recv返回-1的可能原因:
- 客户端强杀进程后,内核生成的FIN/RST包因网络拥塞、防火墙拦截等原因,没有在设置的1秒超时窗口内到达服务端,
recv触发超时返回-1; - 属于典型的TCP半开连接场景:客户端机器直接断网/宕机,根本没有生成关闭连接的报文,服务端永远等不到对端的关闭通知,
recv会持续超时返回-1。
正确的连接可靠性检测方案
1. 先区分recv返回-1的具体原因
recv返回-1时必须校验errno,不能直接判定为断连:
*size = recv(m_connectedSocket, buf, *size, 0); if (*size == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 仅接收超时,连接状态未知,不能判定断开 } else if (errno == ECONNRESET || errno == ETIMEDOUT) { // 对端异常断开,收到RST或TCP探测超时,可判定连接丢失 } else { // 其他错误,按需处理 } } else if (*size == 0) { // 对端正常关闭连接,可判定连接断开 }
2. 开启自定义参数的TCP保活机制
针对TCP半开连接(对端无任何关闭报文发送)的场景,必须开启内核级别的TCP保活探测,避免永久等待:
- 不要使用系统默认的保活参数(默认空闲2小时才开始探测,超时时间过长不符合业务需求),需要自定义保活的空闲时间、探测间隔、失败次数阈值:
int keepalive = 1; // 开启socket保活功能 setsockopt(m_connectedSocket, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); // 以下为Linux平台的自定义保活参数,Windows需使用对应WSAIoctl接口配置 int keep_idle = 10; // 连接空闲10秒后开始发送探测包 int keep_intvl = 3; // 每3秒发送一次探测包 int keep_cnt = 3; // 连续3次探测无响应即判定连接断开 setsockopt(m_connectedSocket, IPPROTO_TCP, TCP_KEEPIDLE, &keep_idle, sizeof(keep_idle)); setsockopt(m_connectedSocket, IPPROTO_TCP, TCP_KEEPINTVL, &keep_intvl, sizeof(keep_intvl)); setsockopt(m_connectedSocket, IPPROTO_TCP, TCP_KEEPCNT, &keep_cnt, sizeof(keep_cnt));
开启后,内核会自动对半开连接发送探测报文,连续无响应后会标记连接为异常,后续recv会返回对应错误码,可直接判定为连接丢失。
3. 叠加应用层心跳机制(可选但更可靠)
如果业务对连接可靠性要求高,或网络环境存在NAT、防火墙等会丢弃长连接空闲流量的设备,建议叠加应用层自定义心跳:
- 每隔固定时间(比如5秒)向客户端发送心跳探测包;
- 累计连续N次(比如3次)未收到客户端的心跳响应,即可主动判定连接丢失,触发重连逻辑。
现有代码中每次
send业务数据后立即recv的逻辑,可直接复用做心跳检测:无业务数据发送时替换为心跳包即可,不需要额外新增接收逻辑。
内容的提问来源于stack exchange,提问作者user14209467
相关产品推荐
相关产品推荐

