4. Socket 选项:SO_REUSEADDR、SO_KEEPALIVE、TCP_NODELAY、SO_LINGER、getsockopt/setsockopt

Socket 选项,说白了就是给内核传话的接口。你告诉它:「这个端口我要复用」「这个连接别断太快」「别攒包了,赶紧发」。我刚开始写网络程序时,总觉得这些选项可有可无。直到线上服务出了几次事故,才老老实实把每个选项都啃了一遍。

今天咱们就聊四个最常用的选项,外加两个操作它们的系统调用。嗯,都是实战中绕不开的东西。

4.1 先学会怎么读写选项:getsockopt / setsockopt

这两个函数是所有选项操作的基础。原型很简单:

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
               void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

参数就四个:

  • sockfd:你要操作的 socket 描述符
  • level:协议层,SOL_SOCKET 是通用层,IPPROTO_TCP 是 TCP 层
  • optname:选项名,比如 SO_REUSEADDR
  • optval:选项值的指针,大部分是 int

我个人习惯在服务端 listen 之前就把所有选项设好,省得后面忘了。

小技巧: 调试时可以用 getsockopt 检查当前选项值,确认内核到底接受了什么。我遇到过几次明明 setsockopt 返回成功,但实际没生效的情况——查一下 optval 就明白了。

4.2 SO_REUSEADDR:端口复用的救星

这个选项可能是你最早遇到的。服务端程序崩溃重启,bind 报错「Address already in use」——就是它没设。

为什么会这样?TCP 关闭时,主动关闭方会进入 TIME_WAIT 状态,持续 2MSL(大约 1-4 分钟)。这段时间里,那个端口还被内核占着。如果不设 SO_REUSEADDR,bind 就会失败。

int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

就这么一行,解决了 90% 的端口冲突问题。我在项目中遇到过好几次:开发环境频繁重启服务,没加这行,每次都要等两分钟才能重新 bind。加了之后,秒级恢复。

注意: SO_REUSEADDR 不是让你随便占端口。它只允许 bind 到 TIME_WAIT 状态的地址。如果端口已经被其他进程 ESTABLISHED 占着,照样 bind 失败。

4.3 SO_KEEPALIVE:检测死连接的探针

你想想看,一个 TCP 连接建立后,如果双方都不发数据,怎么知道对方还活着?默认情况下,内核会一直维护这个连接,直到你主动 close。但要是客户端断电、网线断了,服务端永远不知道。

SO_KEEPALIVE 就是干这个的。开启后,内核会定时发送探测包。如果对方没回应,就重试几次,最终判定连接死亡,关闭 socket。

int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));

默认参数很保守:

参数 默认值 说明
tcp_keepalive_time 7200 秒(2 小时) 空闲多久后开始探测
tcp_keepalive_intvl 75 秒 每次探测间隔
tcp_keepalive_probes 9 次 最多探测次数

也就是说,默认情况下,一个死连接要 2 小时 11 分钟才能被检测到。我建议在应用层自己做心跳,或者调小这些参数。比如:

# 在 /etc/sysctl.conf 中修改
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

这样 5 分钟空闲就开始探测,最多 6 分半钟就能发现死连接。

4.4 TCP_NODELAY:告别 Nagle 算法

Nagle 算法是 TCP 的一个优化:攒够一个 MSS 的数据再发,或者收到前一个包的 ACK 再发。目的是减少小包数量。但有些场景下,这个优化反而成了灾难。

比如实时游戏、即时通讯、SSH 交互——你敲一个字符,它要等 200ms 才发出去,用户体验极差。这时候就需要 TCP_NODELAY。

int optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));

关闭 Nagle 算法后,每次 write 都会立即发送。代价是网络利用率降低,小包变多。但低延迟场景下,这个取舍是值得的。

核心权衡:
  • 高吞吐、大文件传输:保留 Nagle,减少小包
  • 低延迟、交互式应用:开启 TCP_NODELAY

我曾经在一个消息推送服务里踩过坑:没设 TCP_NODELAY,客户端发一条消息要等 200ms 才能收到服务端响应。用户反馈「消息延迟严重」。加上这行后,延迟降到 10ms 以内。

4.5 SO_LINGER:控制 close 的行为

这个选项有点特殊。它决定了 close 函数返回时,内核怎么处理未发送完的数据。

默认行为:close 立即返回,内核在后台继续发送未完成的数据。但如果你想知道数据是否真的发完了,或者想强制关闭连接,就需要 SO_LINGER。

struct linger {
    int l_onoff;   // 0 关闭,非 0 开启
    int l_linger;  // 超时时间(秒)
};

三种模式:

l_onoff l_linger 行为
0 忽略 默认模式,close 立即返回,后台发送
非 0 0 强制关闭,立即发送 RST,丢弃未发送数据
非 0 > 0 阻塞等待,直到数据发完或超时

我建议谨慎使用 l_onoff=1、l_linger=0 的模式。它会发送 RST 而不是正常的 FIN,对端会收到 ECONNRESET 错误。除非你确定要暴力断开,否则别用。

避坑指南: 我曾经在 HTTP 服务器里用了 l_linger=0 来「快速关闭」连接。结果客户端频繁报错「Connection reset by peer」。排查了半天才发现是 SO_LINGER 搞的鬼。后来改成默认行为,问题消失。

4.6 知识体系总览

下面这张图把四个选项和两个系统调用的关系梳理清楚了:

Socket 选项知识体系 getsockopt / setsockopt SO_REUSEADDR 端口复用 / TIME_WAIT SO_KEEPALIVE 心跳探测 / 死连接 TCP_NODELAY 禁用 Nagle / 低延迟 SO_LINGER close 行为 / RST 应用场景:服务端重启 / 心跳检测 / 实时通信 / 连接关闭控制

4.7 实战建议

最后给几个我自己的经验:

  • 服务端一定要设 SO_REUSEADDR,否则重启就是噩梦
  • 长连接服务建议开 SO_KEEPALIVE,但记得调小参数
  • 交互式应用必须设 TCP_NODELAY,延迟敏感场景不能省
  • SO_LINGER 慎用,默认行为通常就够了
  • 用 getsockopt 验证,设完查一下,确认生效

这些选项看着简单,但用错了真会出问题。嗯,记住一句话:选项是给内核的指令,指令错了,程序就错了。