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分钟 才能判定一个连接死亡。对于大多数实时性要求高的场景,这个时间太长了。

注意: 默认值因操作系统而异。Linux 是 7200秒/75秒/9次,但 macOS 和 Windows 可能不同。跨平台开发时,一定要显式设置,不要依赖默认值。

如何开启和设置?

开启 SO_KEEPALIVE 很简单,一个 setsockopt 调用就行。但设置那三个参数,需要用到 TCP_KEEPIDLETCP_KEEPINTVLTCP_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秒)没有回复,就判定连接断开。这个配置对于大多数应用来说,反应速度已经够快了。

我的经验: 不要设得太激进。比如把空闲时间设为5秒,间隔1秒。这样会产生大量探测包,浪费带宽和CPU。我一般建议空闲时间至少30秒,间隔5-10秒,次数3-5次。

保活探测的完整流程

为了让你更直观地理解,我画了一张流程图。它展示了从连接建立到保活探测,再到最终判定断开的完整过程。

TCP保活探测完整流程 TCP连接建立 正常数据交换 空闲计时(TCP_KEEPIDLE) 发送探测包(第1次) 收到ACK回复 → 重置空闲计时 无回复 → 继续探测(最多TCP_KEEPCNT次) 间隔TCP_KEEPINTVL 无回复 → 继续探测(最多TCP_KEEPCNT次)

从图上你可以看到,整个流程其实就是一个「空闲→探测→等待→再探测」的循环。一旦收到回复,计时器就重置,回到空闲状态。如果连续探测都失败,内核就会通知应用层:这个连接已经断了。

如何检测保活事件?

保活探测的结果,最终会通过 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=30TCP_KEEPCNT=5 开始。这个配置比较中庸,既不会太激进,也不会太迟钝。

好了,关于 SO_KEEPALIVE 的用法,我就讲这么多。记住一点:保活是内核给你的一个工具,但不要完全依赖它。结合应用层的心跳,才能让你的网络程序更健壮。


公众号:蓝海资料掘金营,微信deep3321