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 之前就把所有选项设好,省得后面忘了。
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。加了之后,秒级恢复。
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 错误。除非你确定要暴力断开,否则别用。
4.6 知识体系总览
下面这张图把四个选项和两个系统调用的关系梳理清楚了:
4.7 实战建议
最后给几个我自己的经验:
- 服务端一定要设 SO_REUSEADDR,否则重启就是噩梦
- 长连接服务建议开 SO_KEEPALIVE,但记得调小参数
- 交互式应用必须设 TCP_NODELAY,延迟敏感场景不能省
- SO_LINGER 慎用,默认行为通常就够了
- 用 getsockopt 验证,设完查一下,确认生效
这些选项看着简单,但用错了真会出问题。嗯,记住一句话:选项是给内核的指令,指令错了,程序就错了。