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_REUSEADDR、SO_KEEPALIVE等。 - optval:选项值的指针。对于布尔型选项,通常传一个 int 指针,1 表示开启,0 表示关闭。
- optlen:选项值的大小。
返回值:成功返回 0,失败返回 -1 并设置 errno。
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);
}
这段代码,我几乎在每个服务端程序里都会写。尤其是开发调试阶段,频繁重启服务,没有这个选项简直没法干活。
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 等系统参数来调整,或者在应用层自己实现心跳机制。
14.5 TCP_NODELAY:告别 Nagle 算法
Nagle 算法是 TCP 的一个优化策略:它会将多个小数据包合并成一个大的数据包再发送,以减少网络中的小包数量。听起来不错,但对某些应用来说,这简直是灾难。
比如你在写一个即时通讯程序,用户发一条消息,你希望立刻发送出去。但 Nagle 算法会把它攒着,等凑够一个 MSS 再发。用户那边就会感觉「消息延迟了」。这时候就需要禁用 Nagle 算法:
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
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));
14.8 知识体系总览
下面这张图,把 Socket 选项的核心逻辑串起来了:
14.9 实战建议
最后,分享几个我多年积累下来的经验:
- SO_REUSEADDR 是标配:只要是服务端程序,bind 之前一定要设。别问为什么,设就对了。
- 用 getsockopt() 验证:设置完选项后,尤其是缓冲区大小,记得读回来确认一下。内核可能会调整你设置的值。
- 不要滥用 SO_LINGER:除非你明确知道自己在做什么,否则保持默认行为就好。暴力关闭连接会引发各种奇怪的问题。
- TCP_NODELAY 按需使用:低延迟场景才关 Nagle,高吞吐场景开着反而更好。
- 选项设置顺序:先设通用选项(SOL_SOCKET),再设协议特定选项(如 IPPROTO_TCP)。有些选项之间有依赖关系。
好了,关于 Socket 选项,今天就聊到这里。这些选项看似简单,但用好了能解决很多线上问题。下次遇到端口被占用、连接卡顿、缓冲区溢出等问题时,不妨先检查一下你的 Socket 选项设置是否合理。