43. 套接字选项:getsockopt/setsockopt、常用选项详解(SO_REUSEADDR等)
套接字选项,说白了就是给 socket 调参的接口。你写网络程序时,总有一些默认行为需要改一改——比如端口被占用了怎么办?缓冲区太小了怎么办?这时候就得靠 getsockopt 和 setsockopt 这对兄弟了。
我个人习惯,在写服务端程序时,第一件事就是设置 SO_REUSEADDR。为什么?因为我在项目中遇到过太多次「Address already in use」的错误了。你想想看,服务器刚重启,端口还被上一个进程占着,结果新进程起不来——这多尴尬。
43.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 层)、IPPROTO_IP(IP 层) - optname:选项名称,比如
SO_REUSEADDR、SO_KEEPALIVE等 - optval:选项值的缓冲区指针
- optlen:选项值的大小
嗯,这里要注意:getsockopt 的 optlen 是值-结果参数,调用前要传入缓冲区大小,调用后返回实际数据大小。我刚开始写的时候经常忘记初始化这个长度,结果读出来的值全是错的。
43.2 常用选项详解
下面这几个选项,是我在实际项目中用得最多的。我按重要程度排个序:
| 选项名 | 层级 | 作用 | 常用值 |
|---|---|---|---|
| SO_REUSEADDR | SOL_SOCKET | 允许重用本地地址和端口 | 1(启用) |
| SO_KEEPALIVE | SOL_SOCKET | 开启 TCP 保活探测 | 1(启用) |
| SO_RCVBUF / SO_SNDBUF | SOL_SOCKET | 设置接收/发送缓冲区大小 | 根据需求设置 |
| TCP_NODELAY | IPPROTO_TCP | 禁用 Nagle 算法 | 1(禁用) |
| SO_LINGER | SOL_SOCKET | 控制 close() 的行为 | struct linger |
43.3 SO_REUSEADDR —— 最常用的选项
这个选项我几乎在每个服务端程序里都会用。它的作用是:允许在 TIME_WAIT 状态下重用端口。
为什么会这样?TCP 四次挥手后,主动关闭连接的一方会进入 TIME_WAIT 状态,持续 2MSL(大约 1-4 分钟)。如果这时候你重启服务器,bind() 就会失败,报 "Address already in use"。
我曾经在线上环境遇到过这个问题:服务器崩溃后自动重启,结果端口被上一个进程占着,服务起不来,导致整个业务中断了 3 分钟。从那以后,我写服务端代码的第一行就是设置 SO_REUSEADDR。
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
perror("setsockopt SO_REUSEADDR failed");
exit(EXIT_FAILURE);
}
43.4 SO_KEEPALIVE —— 检测死连接
网络编程中,最头疼的问题之一就是「连接断了,但服务端不知道」。客户端突然断电、网线被拔、进程崩溃——这些情况都不会发送 FIN 包,服务端还以为连接好好的。
开启 SO_KEEPALIVE 后,内核会定期发送探测包。如果对方没响应,就会关闭连接。默认的探测间隔是 2 小时,太长了。我一般会配合 /proc/sys/net/ipv4/tcp_keepalive_* 系统参数来调整。
int keepalive = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT 这三个选项在应用层控制保活行为。不过要注意,这些是 Linux 特有的。
43.5 TCP_NODELAY —— 告别延迟
Nagle 算法是为了减少小包数量而设计的。它会将多个小数据包合并成一个发送。但对于实时性要求高的应用(比如游戏、即时通讯),这反而成了累赘。
我记得有一次做股票行情推送系统,数据量不大但要求毫秒级延迟。结果发现 Nagle 算法把多个行情包攒在一起发,客户端收到时已经过了几百毫秒。关闭 Nagle 后,延迟直接降到个位数毫秒。
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
43.6 SO_RCVBUF / SO_SNDBUF —— 调优缓冲区
缓冲区大小直接影响吞吐量。太小了,频繁的系统调用会拖慢性能;太大了,又浪费内存。
我个人习惯:对于高吞吐的服务,把接收缓冲区设为 256KB 或更大。但要注意,内核实际分配的大小可能是你设置值的两倍(因为内核会预留一些空间)。
int rcvbuf = 256 * 1024; // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
43.7 SO_LINGER —— 控制 close() 行为
这个选项有点特殊。它控制的是 close() 函数的行为。默认情况下,close() 会立即返回,但内核会继续尝试发送未发送完的数据。如果你设置了 SO_LINGER,可以改变这个行为。
struct linger ling;
ling.l_onoff = 1; // 启用 linger
ling.l_linger = 0; // 立即关闭,发送 RST
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
当 l_linger = 0 时,close() 会立即发送 RST 包,而不是走四次挥手。这在某些场景下有用,但要注意:对方会收到连接异常断开的错误。
l_linger = 0。它会导致数据丢失,因为未发送完的数据会被直接丢弃。我只有在检测到对方已经不可达时才会用这个。
43.8 知识体系图
下面这张图总结了套接字选项的核心知识结构:
43.9 避坑指南
最后,分享几个我踩过的坑:
- SO_REUSEADDR 不是万能的——它只能解决 TIME_WAIT 的问题。如果端口被其他进程占用了(比如另一个服务在监听),它也没办法。
- 缓冲区设置可能被内核截断——Linux 内核有最大缓冲区限制(
/proc/sys/net/core/rmem_max),你设的值如果超过这个限制,会被静默截断。 - TCP_NODELAY 和 Cork 的冲突——如果你同时设置了 TCP_NODELAY 和 TCP_CORK,后者的优先级更高。我曾经因为这个排查了半天,最后发现是配置冲突了。
- SO_LINGER 的副作用——设置
l_linger = 0后,close() 会立即返回,但内核会发送 RST。这会导致对方收到 ECONNRESET 错误,而不是正常的 EOF。
嗯,套接字选项这块内容其实不难,但细节很多。你想想看,一个选项设错了,可能整个网络程序的行为就变了。我建议你在写代码时,每个选项都加上注释,说明为什么要这么设。这样半年后回头看代码,自己也能明白当初的意图。
getsockopt 打印出所有选项的当前值,可以帮助你快速定位问题。我写了一个小工具函数,专门用来 dump socket 的状态,线上排查问题时特别好用。
好了,这一章的内容就到这里。套接字选项是网络编程的基础功,掌握了它们,你就能更好地控制 socket 的行为,写出更健壮的网络程序。
公众号:蓝海资料掘金营,微信deep3321