TCP服务器断开客户端的方法

在一个项目中使用设计了一下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、先看连接 A(62541)完整时序

  1. 三次握手正常建立连接。
  2. 客户端发 PSH+ACK 上报 14 字节业务数据;服务端回 ACK 确认。
  3. 服务端发出 PSH+ACK(15 字节应答),这个报文没有收到客户端 ACK ,开始TCP Retransmission重传(连续多次重传服务端的应答包)。
  4. 重传超时还收不到对端 ACK → 客户端直接发送 RST|ACK(No.7346)把整条连接干掉

⚠️关键点:这条连接在服务端还没机会走到 TCP Keep‑Alive 定时器,就因为服务端下行报文持续重传失败,客户端直接 RST 重置连接 。 服务端此时状态:在不断重传未被 ACK 的业务数据,TCP keepalive 不会在有未确认待重传数据时触发 。 TCP Keep‑Alive 仅针对连接完全空闲,没有待发送 / 待确认数据的 socket 才会启动保活探测;当 socket 有未 ACK 的出站报文,内核优先处理重传定时器,keepalive 定时器被搁置。

2、连接 B(56225)正常时序

  1. 三次握手建立;双向收发多轮业务数据。
  2. 业务交互结束,双向没有新业务报文,Socket 进入完全空闲状态
  3. 此时服务端 socket 没有待确认的未完成报文,内核 TCP Keep‑Alive 定时器开始工作 ,连续发出TCP Keep‑Alive探测包。
  4. 多次 keep‑alive 探测无应答后,服务端内核发出 RST 把这条连接销毁 ,就是抓包看到的 No.8327 9500 → 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(客户端 56225)
Socket 状态 服务端存在未 ACK 的下行报文,非空闲 IDLE 状态 全部报文完成 ACK 确认,socket 空闲 IDLE
内核定时器 重传定时器生效,不停重传业务报文 Keep‑Alive 保活定时器生效,发送 keep‑alive 探测
RST 来源 客户端内核回 RST(收到大量重复重传包) 服务端内核发出 RST(keepalive 探测全部失败)
能否看到 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包来关闭,应用层的代码写法如下:

cpp 复制代码
/*服务端发送RST包关闭连接, 因为客户端发送完数据立即断电 */
struct linger lng = { 1, 0 };
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng));
close(fd)
相关推荐
啦啦啦啦啦zzzz1 小时前
5种IO模型
运维·服务器·网络·c++
FreeTinker1 小时前
企业存储服务器NAS的选型逻辑与补充路径
运维·服务器
打妖妖灵滴哪吒1 小时前
OSI七层网络参考模型(图文版)
网络
CodeJourney_J2 小时前
网络安全简述-加密技术
网络·安全·密码学
xiaoxiangsiyan3 小时前
企业网络运维自动化与管理协议深度指南
linux·运维·服务器·网络·学习·自动化
風穆3 小时前
多云时代的“服务器比价网”:OopsVPS 实用主义指南
服务器
wgh6533 小时前
旁挂组网双机热备负载分担
运维·服务器
长江&后浪3 小时前
华为M-LAG配置小实验
网络
2301_777998343 小时前
网络基础(一):协议与网络传输基本流程
linux·网络