14、Socket选项:setsockopt()与getsockopt(),SO_REUSEADDR等常用选项

Socket 选项,说白了就是给 Socket 加的各种「开关」和「旋钮」。

我刚开始写网络程序时,总觉得 Socket 创建好、连上就完事了。后来线上出了几次问题,才发现很多坑都是因为没调好这些选项。比如端口被占用了、缓冲区太小导致丢包了……嗯,今天我们就来聊聊怎么用 setsockopt()getsockopt() 这两个函数,把 Socket 调教得服服帖帖。

14.1 两个核心函数:setsockopt() 与 getsockopt()

这两个函数,一个负责「写」选项,一个负责「读」选项。先看它们的原型:

#include <sys/socket.h>

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

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

参数说明:

  • sockfd:Socket 描述符,就是你要操作的那个 Socket。
  • level:选项所在的协议层。常用的是 SOL_SOCKET(通用 Socket 层)和 IPPROTO_TCP(TCP 层)。
  • optname:选项的名字,比如 SO_REUSEADDRSO_KEEPALIVE 等。
  • optval:选项值的指针。对于布尔型选项,通常传一个 int 指针,1 表示开启,0 表示关闭。
  • optlen:选项值的大小。

返回值:成功返回 0,失败返回 -1 并设置 errno。

小提示:我个人习惯在调用 setsockopt() 后立即检查返回值。曾经有一次我忘了检查,结果 SO_REUSEADDR 没设上,线上服务重启时端口被占用了好几分钟……从那以后,我每个选项设置完都会加个 assert 或日志。

14.2 最常用的选项:SO_REUSEADDR

这个选项,可以说是 Socket 编程里「救火队员」级别的存在。

它解决什么问题?

正常情况下,一个端口被某个进程绑定后,即使这个进程退出了,端口也会进入 TIME_WAIT 状态,持续一段时间(通常是 2MSL,大约 1-4 分钟)。在这段时间内,其他进程无法绑定同一个端口。

你想想看,如果你的服务器崩溃了,想立刻重启,结果发现端口还被占着……是不是很崩溃?

SO_REUSEADDR 的作用:允许在端口处于 TIME_WAIT 状态时,立即重新绑定该端口。

int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
    perror("setsockopt SO_REUSEADDR");
    exit(EXIT_FAILURE);
}

这段代码,我几乎在每个服务端程序里都会写。尤其是开发调试阶段,频繁重启服务,没有这个选项简直没法干活。

注意:SO_REUSEADDR 必须在 bind() 之前调用。我曾经犯过这个错——先 bind 再设置选项,结果选项根本没生效。记住顺序:socket() → setsockopt() → bind()。

14.3 其他常用选项

除了 SO_REUSEADDR,还有几个选项我经常用到,这里列出来给大家参考:

选项名 层级 作用 我常用的场景
SO_REUSEADDR SOL_SOCKET 允许重用本地地址和端口 服务端快速重启
SO_KEEPALIVE SOL_SOCKET 开启 TCP 保活探测 检测死连接
SO_RCVBUF SOL_SOCKET 设置接收缓冲区大小 高吞吐场景
SO_SNDBUF SOL_SOCKET 设置发送缓冲区大小 高吞吐场景
TCP_NODELAY IPPROTO_TCP 禁用 Nagle 算法 低延迟交互
SO_LINGER SOL_SOCKET 控制 close() 的行为 优雅关闭连接

14.4 SO_KEEPALIVE:检测死连接

做服务端开发时,最怕的就是「半开连接」——客户端已经崩溃了,但服务端还傻傻地等着。SO_KEEPALIVE 就是用来解决这个问题的。

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

开启后,如果连接空闲超过 2 小时(默认值),内核会自动发送探测包。如果对方没响应,连接会被关闭。

不过说实话,2 小时的默认间隔太长了。我一般会配合 /proc/sys/net/ipv4/tcp_keepalive_time 等系统参数来调整,或者在应用层自己实现心跳机制。

避坑指南:我曾经在一个长连接项目里,只依赖 SO_KEEPALIVE 来检测客户端掉线。结果发现,有些客户端只是网络波动,短暂断连后又恢复了,但服务端已经把连接关了。后来我改用应用层心跳 + 超时重试机制,才解决了这个问题。

14.5 TCP_NODELAY:告别 Nagle 算法

Nagle 算法是 TCP 的一个优化策略:它会将多个小数据包合并成一个大的数据包再发送,以减少网络中的小包数量。听起来不错,但对某些应用来说,这简直是灾难。

比如你在写一个即时通讯程序,用户发一条消息,你希望立刻发送出去。但 Nagle 算法会把它攒着,等凑够一个 MSS 再发。用户那边就会感觉「消息延迟了」。这时候就需要禁用 Nagle 算法:

int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
小技巧:如果你写的是 Web 服务器或文件传输程序,Nagle 算法反而能提升效率。只有对延迟敏感的应用(如游戏、即时通讯),才需要禁用。别一上来就无脑关掉。

14.6 SO_RCVBUF / SO_SNDBUF:调大缓冲区

默认的 Socket 缓冲区大小通常只有几十 KB。对于高吞吐的场景,比如视频流、大文件传输,这个值太小了,会导致频繁的丢包和重传。

int rcvbuf = 1024 * 1024;  // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

int sndbuf = 1024 * 1024;  // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));

不过要注意,内核会对设置的值做上下限限制。你可以用 getsockopt() 来确认实际生效的值:

int actual_rcvbuf;
socklen_t len = sizeof(actual_rcvbuf);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &actual_rcvbuf, &len);
printf("实际接收缓冲区大小: %d\n", actual_rcvbuf);

14.7 SO_LINGER:控制 close() 的行为

这个选项有点特殊,它控制着调用 close() 时,Socket 的行为。

默认情况下,close() 会立即返回,但内核会继续尝试发送缓冲区中剩余的数据。如果设置了 SO_LINGER 且 l_onoff 为 1,l_linger 为 0,那么 close() 会立即发送 RST 包,丢弃所有未发送的数据。这通常用于「强制关闭」。

struct linger l;
l.l_onoff = 1;   // 开启
l.l_linger = 0;  // 立即超时
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
警告:设置 l_linger 为 0 的 close() 是「暴力关闭」,会发送 RST 而不是正常的 FIN。对方会收到一个连接重置错误。除非你确定要这么做(比如检测到对方已经挂了),否则不要轻易使用。

14.8 知识体系总览

下面这张图,把 Socket 选项的核心逻辑串起来了:

Socket 选项核心逻辑 Socket 选项 setsockopt() 写选项 getsockopt() 读选项 SO_REUSEADDR SO_KEEPALIVE TCP_NODELAY SO_RCVBUF/SNDBUF 必须在 bind() 之前设置,否则可能不生效

14.9 实战建议

最后,分享几个我多年积累下来的经验:

  1. SO_REUSEADDR 是标配:只要是服务端程序,bind 之前一定要设。别问为什么,设就对了。
  2. 用 getsockopt() 验证:设置完选项后,尤其是缓冲区大小,记得读回来确认一下。内核可能会调整你设置的值。
  3. 不要滥用 SO_LINGER:除非你明确知道自己在做什么,否则保持默认行为就好。暴力关闭连接会引发各种奇怪的问题。
  4. TCP_NODELAY 按需使用:低延迟场景才关 Nagle,高吞吐场景开着反而更好。
  5. 选项设置顺序:先设通用选项(SOL_SOCKET),再设协议特定选项(如 IPPROTO_TCP)。有些选项之间有依赖关系。

好了,关于 Socket 选项,今天就聊到这里。这些选项看似简单,但用好了能解决很多线上问题。下次遇到端口被占用、连接卡顿、缓冲区溢出等问题时,不妨先检查一下你的 Socket 选项设置是否合理。


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