47、SO_KEEPALIVE选项:保活探测、探测间隔设置、TCP_KEEPIDLE等
说实话,TCP 的保活机制,我早年做嵌入式网络设备时,一直觉得它是个「鸡肋」功能。为什么?因为很多人以为它能让连接「永远不断」,其实不是这么回事。它只是帮你检测一下——对端是不是已经挂了,或者网络是不是已经断了。
但后来我接手过一个远程监控项目,服务器端维护了上千个长连接。有些客户端半夜断电,第二天早上才重启。如果没有保活机制,服务器上那些「僵尸连接」会一直占着资源,直到超时回收。嗯,从那时起,我才真正重视起 SO_KEEPALIVE 来。
什么是保活探测?
说白了,就是 TCP 协议栈每隔一段时间,自动给对端发一个空的数据包(ACK 包,不带数据)。如果对端正常,会回复一个 ACK。如果连续几次都没回复,内核就认为这个连接已经死了,然后通知应用程序。
你想想看,这比你自己写心跳包要省事多了。内核帮你做了底层的事情,你只需要打开一个选项,设置几个参数就行。
核心参数:TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT
这三个参数,我习惯叫它们「保活三兄弟」。它们共同决定了保活探测的行为。
| 参数名 | 含义 | 默认值(Linux) | 说明 |
|---|---|---|---|
TCP_KEEPIDLE |
空闲多久后开始第一次探测 | 7200 秒(2小时) | 连接上如果没有数据交换,2小时后才发第一个探测包 |
TCP_KEEPINTVL |
每次探测的间隔时间 | 75 秒 | 如果没收到回复,每隔75秒再发一次 |
TCP_KEEPCNT |
最多探测几次 | 9 次 | 连续9次无回复,就判定连接断开 |
默认值其实挺保守的。2小时才开始探测,9次间隔75秒,总共要 2小时 + 9×75秒 ≈ 2小时11分钟 才能判定一个连接死亡。对于大多数实时性要求高的场景,这个时间太长了。
如何开启和设置?
开启 SO_KEEPALIVE 很简单,一个 setsockopt 调用就行。但设置那三个参数,需要用到 TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT 这些选项。它们定义在 <netinet/tcp.h> 里。
我贴一段我常用的代码模板,你可以直接拿去用:
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h> // 注意这个头文件
#include <stdio.h>
#include <unistd.h>
int enable_keepalive(int fd) {
int keepalive = 1;
int keepidle = 60; // 60秒空闲后开始探测
int keepintvl = 10; // 每次探测间隔10秒
int keepcnt = 3; // 最多探测3次
// 第一步:开启保活
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,
&keepalive, sizeof(keepalive)) < 0) {
perror("setsockopt SO_KEEPALIVE");
return -1;
}
// 第二步:设置空闲时间
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE,
&keepidle, sizeof(keepidle)) < 0) {
perror("setsockopt TCP_KEEPIDLE");
return -1;
}
// 第三步:设置探测间隔
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL,
&keepintvl, sizeof(keepintvl)) < 0) {
perror("setsockopt TCP_KEEPINTVL");
return -1;
}
// 第四步:设置探测次数
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT,
&keepcnt, sizeof(keepcnt)) < 0) {
perror("setsockopt TCP_KEEPCNT");
return -1;
}
printf("保活已开启:空闲%d秒后探测,间隔%d秒,最多%d次\n",
keepidle, keepintvl, keepcnt);
return 0;
}
这段代码里,我把空闲时间设成了60秒,间隔10秒,最多3次。也就是说,如果连接空闲1分钟,就开始发探测包。如果30秒内(3次×10秒)没有回复,就判定连接断开。这个配置对于大多数应用来说,反应速度已经够快了。
保活探测的完整流程
为了让你更直观地理解,我画了一张流程图。它展示了从连接建立到保活探测,再到最终判定断开的完整过程。
从图上你可以看到,整个流程其实就是一个「空闲→探测→等待→再探测」的循环。一旦收到回复,计时器就重置,回到空闲状态。如果连续探测都失败,内核就会通知应用层:这个连接已经断了。
如何检测保活事件?
保活探测的结果,最终会通过 read() 或 write() 的返回值体现出来。具体来说:
- 连接正常: 探测包收到回复,应用层无感知,一切照旧。
- 连接断开(对端崩溃): 连续探测失败后,内核会关闭连接。此时
read()返回 0(EOF),write()会触发SIGPIPE信号或返回EPIPE错误。 - 对端无响应但网络恢复: 如果探测期间网络闪断,但很快恢复,保活机制可能会误判。不过这种情况很少见,因为 TCP 本身有重传机制。
我建议你在应用层也做一层心跳检测,和内核保活配合使用。为什么?因为内核保活只能告诉你「TCP 连接是否还通」,但应用层的逻辑可能更复杂。比如对端进程卡死了,但 TCP 连接还活着,内核保活是检测不出来的。
避坑指南
我曾经在一个项目中,把 TCP_KEEPIDLE 设成了 5 秒,结果线上服务器 CPU 飙升。排查了半天才发现,因为探测包太频繁,导致内核协议栈负载过高。后来改成了 60 秒,问题就解决了。
还有一次,我在 macOS 上开发,发现 TCP_KEEPIDLE 这个选项居然不生效。查了文档才知道,macOS 用的是 TCP_KEEPALIVE(注意名字不一样),而且单位是毫秒。所以跨平台开发时,一定要针对不同系统做适配。
- Linux 上
TCP_KEEPIDLE单位是秒,macOS 上TCP_KEEPALIVE单位是毫秒。 - Windows 上使用
SO_KEEPALIVE后,需要通过注册表或SIO_KEEPALIVE_VALS来设置参数。 - 保活探测包会增加网络流量,对于移动端或低带宽环境,要谨慎使用。
什么时候该用保活?
我个人总结了几种典型场景:
- 长连接服务器: 比如聊天服务器、游戏服务器,需要及时清理死连接。
- NAT 穿透: 很多 NAT 设备会超时关闭空闲连接。保活探测可以保持连接活跃,防止被 NAT 踢掉。
- 嵌入式设备: 设备可能突然断电或断网,服务器需要快速感知。
但如果你只是做短连接(比如 HTTP 请求-响应模式),那保活基本用不上。连接用完就关,没必要浪费资源去探测。
TCP_KEEPIDLE=300(5分钟)、TCP_KEEPINTVL=30、TCP_KEEPCNT=5 开始。这个配置比较中庸,既不会太激进,也不会太迟钝。
好了,关于 SO_KEEPALIVE 的用法,我就讲这么多。记住一点:保活是内核给你的一个工具,但不要完全依赖它。结合应用层的心跳,才能让你的网络程序更健壮。
公众号:蓝海资料掘金营,微信deep3321