跳转至

Socket 选项

是什么 / 解决什么问题

通过 setsockopt/getsockopt 控制 socket 的行为参数。覆盖最常用的选项及其实际影响。

使用模式

#include <sys/socket.h>
#include <netinet/tcp.h>

// 设置选项
int val = 1;
setsockopt(fd, level, optname, &val, sizeof(val));

// 获取选项
int val;
socklen_t len = sizeof(val);
getsockopt(fd, level, optname, &val, &len);

SOL_SOCKET 层(通用选项)

SO_REUSEADDR

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

作用: - 允许 bind 到处于 TIME_WAIT 的地址 - 允许 bind 到 0.0.0.0 即使已有 socket bind 到具体 IP 的同一端口

几乎所有服务器都应该设置。不设置的话,重启服务器可能报 EADDRINUSE

SO_REUSEPORT

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));

作用:允许多个 socket(多个进程/线程)bind 到完全相同的 address:port。内核对新连接做负载均衡。

线程1: socket() → bind(:8080) → listen() → accept()
线程2: socket() → bind(:8080) → listen() → accept()
线程3: socket() → bind(:8080) → listen() → accept()
           ↑ 内核自动分配新连接到不同线程

优势:消除共享 listen socket 的 accept 锁竞争。Nginx 1.9.1+ 支持。

SO_KEEPALIVE

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));

开启 TCP keep-alive 探测。搭配 TCP_KEEPIDLE/INTVL/CNT 使用(见 TCP 行为篇)。

SO_LINGER

struct linger lg = {
    .l_onoff = 1,
    .l_linger = 0,  // 秒数
};
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));

控制 close() 的行为:

l_onoff l_linger close() 行为
0 (忽略) 默认:后台发送缓冲区数据,立即返回
1 0 发送 RST,丢弃缓冲区,立即返回(硬关闭)
1 >0 阻塞等待数据发完或超时,超时则 RST

l_linger=0 的用途:避免 TIME_WAIT(代价是对端可能丢数据)。

SO_SNDBUF / SO_RCVBUF

int size = 262144;  // 256KB
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

// 注意:内核会将设置值翻倍(用于内部簿记开销)
// 设 256KB,实际分配 512KB
// 受 /proc/sys/net/core/rmem_max 和 wmem_max 限制

何时调整: - 高带宽长延迟链路(BDP = bandwidth * RTT) - 缓冲区 < BDP 时无法跑满带宽

SO_RCVTIMEO / SO_SNDTIMEO

struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
// 超时后 read/write 返回 -1, errno = EAGAIN/EWOULDBLOCK

注意:这不是 connect 超时。connect 超时需要非阻塞+epoll 或 TCP_USER_TIMEOUT。

IPPROTO_TCP 层(TCP 特定选项)

TCP_NODELAY

int on = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));

关闭 Nagle 算法。小数据立即发送,不等待凑够 MSS。

应该设置的场景: - 请求-响应协议(HTTP、RPC) - 实时应用(游戏、交互式终端) - 已经在应用层做了写合并

不该设置的场景: - 大量微小写入且不做合并(会产生大量小包)

TCP_CORK

int on = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &on, sizeof(on));
// 此后的 write 全部缓冲,不发送
write(fd, header, header_len);
write(fd, body, body_len);
on = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &on, sizeof(on));
// 取消 cork,一次性发出所有缓冲数据

类似 Nagle 但是手动控制。适合发送 HTTP 响应(header + body 合并为少量 TCP 段)。

TCP_QUICKACK

int on = 1;
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &on, sizeof(on));

禁用 Delayed ACK,收到数据后立即回 ACK。注意:这不是持久设置,内核可能在后续操作中重新启用 Delayed ACK。需要在每次读取后重新设置。

TCP_USER_TIMEOUT

unsigned int timeout = 5000;  // 5秒
setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout));

如果已发送数据在指定时间内未被确认,则放弃连接。比 keep-alive 更精确的死连接检测。

TCP_FASTOPEN

// 服务端:允许 TFO
int qlen = 128;
setsockopt(listen_fd, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));

// 客户端:connect 时发送数据(省一个 RTT)
sendto(fd, data, data_len, MSG_FASTOPEN,
       (struct sockaddr *)&server, sizeof(server));

首次连接还是三次握手,后续连接可以在 SYN 中携带数据,减少一个 RTT。

TCP_CONGESTION

char algo[] = "bbr";
setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, algo, strlen(algo));

Per-socket 设置拥塞控制算法。

TCP_INFO

struct tcp_info info;
socklen_t len = sizeof(info);
getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &len);

printf("rtt: %u us\n", info.tcpi_rtt);
printf("cwnd: %u\n", info.tcpi_snd_cwnd);
printf("retrans: %u\n", info.tcpi_total_retrans);
printf("unacked: %u\n", info.tcpi_unacked);

获取连接的详细 TCP 统计信息。对性能诊断极有价值。

性能考量

服务器推荐配置

// 典型高性能 TCP 服务器的 socket 选项
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));

// 对 accept 得到的连接 fd
setsockopt(conn_fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));

// 可选:快速检测死连接
setsockopt(conn_fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
int idle = 60, intvl = 10, cnt = 3;
setsockopt(conn_fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(conn_fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
setsockopt(conn_fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));

需要根据场景决定的选项

场景 推荐
低延迟 RPC TCP_NODELAY + TCP_QUICKACK
高吞吐文件传输 大 SO_SNDBUF/RCVBUF + TCP_CORK
短连接 Web 服务 SO_REUSEPORT + TCP_FASTOPEN
跨 IDC 长肥管道 大 buffer + BBR

常见陷阱与 FAQ

1. SO_SNDBUF 设置后实际是双倍

int size = 65536;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, &len);
// size = 131072 (翻倍了)
// 这是正常的,内核保留一半用于内部管理

2. TCP_NODELAY 和 TCP_CORK 互斥

同时设置两个的行为未定义。通常选一个: - TCP_NODELAY:所有写立即发 - TCP_CORK:手动批量控制

3. SO_RCVTIMEO 对 epoll 无效

超时选项只对阻塞式 read/write 生效。用 epoll 时靠 epoll_wait 的 timeout 参数控制。

观测与调试

# 查看 socket 选项的当前值
python3 -c "
import socket
s = socket.socket()
s.connect(('localhost', 8080))
print('TCP_NODELAY:', s.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
print('SO_SNDBUF:', s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF))
print('SO_RCVBUF:', s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))
"

# ss 显示连接参数
ss -tnpi dst :8080

# strace 看 setsockopt 调用
strace -e trace=setsockopt -f ./server

延伸阅读

  • man 7 socket — SOL_SOCKET 层选项
  • man 7 tcp — IPPROTO_TCP 层选项
  • man 7 ip — IPPROTO_IP 层选项
  • /proc/sys/net/ipv4/tcp_* — 系统级 TCP 参数