43. 套接字选项:getsockopt/setsockopt、常用选项详解(SO_REUSEADDR等)

套接字选项,说白了就是给 socket 调参的接口。你写网络程序时,总有一些默认行为需要改一改——比如端口被占用了怎么办?缓冲区太小了怎么办?这时候就得靠 getsockoptsetsockopt 这对兄弟了。

我个人习惯,在写服务端程序时,第一件事就是设置 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_REUSEADDRSO_KEEPALIVE
  • optval:选项值的缓冲区指针
  • optlen:选项值的大小

嗯,这里要注意:getsockoptoptlen 是值-结果参数,调用前要传入缓冲区大小,调用后返回实际数据大小。我刚开始写的时候经常忘记初始化这个长度,结果读出来的值全是错的。

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);
}
注意:SO_REUSEADDR 只对 bind() 阶段有效。如果你已经 bind 成功了,再设置这个选项是没用的。一定要在 bind() 之前设置。

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_KEEPIDLETCP_KEEPINTVLTCP_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));
核心要点:设置缓冲区时,最好先调用 getsockopt 获取当前值,再根据需求调整。不要盲目设得太大,尤其是嵌入式设备上。

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 知识体系图

下面这张图总结了套接字选项的核心知识结构:

套接字选项核心知识体系 套接字选项 函数接口 getsockopt() — 读取选项值 setsockopt() — 设置选项值 参数:level, optname, optval 常用选项 SO_REUSEADDR — 端口重用 SO_KEEPALIVE — 保活探测 TCP_NODELAY — 禁用Nagle SO_RCVBUF/SO_SNDBUF — 缓冲区 SO_LINGER — close行为控制 协议层级 SOL_SOCKET — 通用套接字层 IPPROTO_TCP — TCP协议层 IPPROTO_IP — IP协议层 核心原则:在 bind() 之前设置选项 先 getsockopt 获取当前值,再 setsockopt 调整

43.9 避坑指南

最后,分享几个我踩过的坑:

  1. SO_REUSEADDR 不是万能的——它只能解决 TIME_WAIT 的问题。如果端口被其他进程占用了(比如另一个服务在监听),它也没办法。
  2. 缓冲区设置可能被内核截断——Linux 内核有最大缓冲区限制(/proc/sys/net/core/rmem_max),你设的值如果超过这个限制,会被静默截断。
  3. TCP_NODELAY 和 Cork 的冲突——如果你同时设置了 TCP_NODELAY 和 TCP_CORK,后者的优先级更高。我曾经因为这个排查了半天,最后发现是配置冲突了。
  4. SO_LINGER 的副作用——设置 l_linger = 0 后,close() 会立即返回,但内核会发送 RST。这会导致对方收到 ECONNRESET 错误,而不是正常的 EOF。

嗯,套接字选项这块内容其实不难,但细节很多。你想想看,一个选项设错了,可能整个网络程序的行为就变了。我建议你在写代码时,每个选项都加上注释,说明为什么要这么设。这样半年后回头看代码,自己也能明白当初的意图。

调试小技巧:getsockopt 打印出所有选项的当前值,可以帮助你快速定位问题。我写了一个小工具函数,专门用来 dump socket 的状态,线上排查问题时特别好用。

好了,这一章的内容就到这里。套接字选项是网络编程的基础功,掌握了它们,你就能更好地控制 socket 的行为,写出更健壮的网络程序。


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