
在一个项目中使用设计了一下TCP服务器使用epoll机制来提高服务器的性能由于客户端的wifi模组发完数据变会断电不会走正常的TCP链接关闭流程因此TCP服务器端需要主动关闭不在使用的链接。A、TCP keepalive机制为了不多写代码我首先想到的是keepalive机制让TCP协议端检测客户端的链接如果没有反应就在设定的超时时间后主动关闭链接。设置一个socket的keepalive超时的方法如下/********************************************************************************************************* ** Function name: set_tcp_timeout() ** Descriptions: set TCP timeout parameters ** input parameters: fd, keep_idle, keep_interval, keep_count ** output parameters: None ** Returned value: None *********************************************************************************************************/ void set_tcp_timeout(int fd, int keep_idle, int keep_interval, int keep_count) { /* 开启TCP保活机制 */ int keep_alive 1; setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, keep_alive, sizeof(keep_alive)); /* 30秒内无数据 → 开始探测 */ setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, keep_idle, sizeof(keep_idle)); /* 探测间隔1秒可改 */ setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, keep_interval, sizeof(keep_interval)); /* 探测1次无响应就断开 30秒无数据立刻断开 */ setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, keep_count, sizeof(keep_count)); }采用这个机制后连接6个客户端正常处理当连接到18个客户端时服务端程序一启动后有几个客户端的链接长时间不释放时间长达15分钟后再翻译程序内部设置的keepalive时间为20秒应该有20秒就会释放 抓包分析如下Wireshark 抓包分析62541 与 56225 两条连接行为差异节点说明服务端192.168.68.2:9500客户端192.168.68.105连接 A客户端端口62541异常客户端主动发 RST 断开服务端没有执行 keepalive 探测连接 B客户端端口56225正常服务端 9500 发出TCP Keep‑Alive最后服务端发出 RST 关闭连接1、先看连接 A62541完整时序三次握手正常建立连接。客户端发 PSHACK 上报 14 字节业务数据服务端回 ACK 确认。服务端发出 PSHACK15 字节应答这个报文没有收到客户端 ACK开始TCP Retransmission重传连续多次重传服务端的应答包。重传超时还收不到对端 ACK →客户端直接发送 RST|ACKNo.7346把整条连接干掉。⚠️关键点这条连接在服务端还没机会走到 TCP Keep‑Alive 定时器就因为服务端下行报文持续重传失败客户端直接 RST 重置连接。 服务端此时状态在不断重传未被 ACK 的业务数据TCP keepalive 不会在有未确认待重传数据时触发。 TCP Keep‑Alive 仅针对连接完全空闲没有待发送 / 待确认数据的 socket 才会启动保活探测当 socket 有未 ACK 的出站报文内核优先处理重传定时器keepalive 定时器被搁置。2、连接 B56225正常时序三次握手建立双向收发多轮业务数据。业务交互结束双向没有新业务报文Socket 进入完全空闲状态。此时服务端 socket 没有待确认的未完成报文内核 TCP Keep‑Alive 定时器开始工作连续发出TCP Keep‑Alive探测包。多次 keep‑alive 探测无应答后服务端内核发出 RST 把这条连接销毁就是抓包看到的 No.83279500 → 56225 [RST,ACK]。3、核心根本原因为什么两条连接表现不一样① TCP Keep‑Alive 的内核重要规则TCP Keep‑Alive 只对 IDLE 空闲连接生效只要 socket 还有未收到 ACK 的发送队列数据keep‑alive 定时器不会运行内核跑的是重传定时器。连接 A (62541)服务端发送 15 字节应答包客户端没有回复 ACK。服务端发送队列存在未确认数据 → 内核运行重传计时器反复重传该数据包keepalive 完全不启动。 多次重传后客户端这边感知异常直接从客户端侧发出 RST 重置连接。现象你看不到 9500 端口的 keep‑alive 包直接看到客户端 RST。连接 B (56225)所有双向业务数据包全部得到对方 ACK 确认发送队列为空socket 进入 IDLE 空闲此时才启动 TCP keep‑alive多次探测失败后服务端内核发出 RST 断开。② 为什么连接 A 客户端会发出 RST抓包里看到服务端不停重传9500→62541 [PSH,ACK]客户端这边收到这些重复的重传报文。 客户端 TCP 协议栈发现当前接收窗口已经 ACK 过该序列号的数据收到重复的旧报文客户端 TCP 栈直接回复 RST 来销毁这条异常连接。不是业务应用代码调用 close是内核 TCP 协议栈自动回 RST应对乱序 / 重复报文。4、两条链路差异总结表表格项目连接 A客户端 62541连接 B客户端 56225Socket 状态服务端存在未 ACK 的下行报文非空闲 IDLE 状态全部报文完成 ACK 确认socket 空闲 IDLE内核定时器重传定时器生效不停重传业务报文Keep‑Alive 保活定时器生效发送 keep‑alive 探测RST 来源客户端内核回 RST收到大量重复重传包服务端内核发出 RSTkeepalive 探测全部失败能否看到 9500 的 Keep‑Alive 包❌看不到keepalive 不会启动✅可以看到多条 TCP Keep‑Alive 报文一句话概括现象不是服务端没开启 keep‑alive第一条连接因为存在未确认重传报文还没轮到 keep‑alive 上场连接就已经被客户端 RST 干掉第二条链路业务全部 ACK 完毕空闲下来keep‑alive 才正常工作。因此keepalive机制对于处理客户端链接时按设定的超时时间关闭链接的功能通过测试发现如果服务端给客户端发送数据客户端没有应答就像上面的连接A的状态服务器会进入Linux 重传超时总耗时计算 (tcp_retries2)不会发送keepalive包最后过超时时间约15分钟后TCP协议栈才会主动关闭这个连接导致这个连接在服务器端关闭的时间较长。B、解决办法找到问题的原因后解决办法很简单就是在应用层检测客户端长时间未发送数据就主动关闭连接关闭时由于客户端是断电的不能走正常的TCP close流程需要走让服务器送RST包来关闭应用层的代码写法如下/*服务端发送RST包关闭连接, 因为客户端发送完数据立即断电 */ struct linger lng { 1, 0 }; setsockopt(fd, SOL_SOCKET, SO_LINGER, lng, sizeof(lng)); close(fd)