POSIX Sockets API:使用技巧与常见陷阱
POSIX sockets API(也叫 Berkeley 或 BSD sockets)是 Linux 上能用的最底层的网络接口。它也布满了微妙的失败模式。除非你有充分的理由直接在这一层工作,否则类似 libuv、libevent 或 ASIO 的跨平台库会替你处理掉大部分问题。
本文记录你如果直接对着裸 API 写代码时会遇到的那些坑,以及每个的正确处理方式。
抑制 SIGPIPE
对一个远端已关闭的 TCP socket 写入,会让内核把 SIGPIPE 投递给进程。SIGPIPE 的默认行为是终止进程。在多线程程序中,信号处理器是进程全局的,极易出现竞态。
Linux 与大多数 BSD(OpenBSD、FreeBSD、NetBSD)
在每次 send 调用上传 MSG_NOSIGNAL:
ssize_t n = send(fd, msg, len, MSG_NOSIGNAL);
调用返回 -1、errno = EPIPE,而不会引发信号。
macOS
MSG_NOSIGNAL 在 macOS 上不可用。改用 SO_NOSIGPIPE socket 选项:
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)) == -1) {
perror("setsockopt");
return -1;
}
可移植的兜底
在进程范围内忽略 SIGPIPE:
signal(SIGPIPE, SIG_IGN);
到处能用,但很粗暴 —— 它会抑制进程中所有文件描述符的 SIGPIPE,包括到子进程的管道。
处理 EINTR
任何阻塞系统调用都可能被信号打断,返回 -1、errno = EINTR。是否自动重启取决于安装信号处理器时是否设置了 SA_RESTART,以及具体的系统调用。Linux 上的 epoll_wait 无论是否设 SA_RESTART 都会返回 EINTR。
遇到 EINTR 要检查并重试:
retry:
ssize_t n = recv(fd, buf, len, 0);
if (n == -1) {
if (errno == EINTR) goto retry;
perror("recv");
return -1;
}
同样的模式适用于 send、connect、accept、epoll_wait 和其他任何阻塞调用。
异步主机名解析
getaddrinfo 是 POSIX 的主机名解析函数。它是同步的,等 DNS 回包时可能阻塞数秒,不能直接用在事件循环里。
如果你的代码只需要处理 IP 字面量,把 AI_NUMERICHOST 传入以让调用非阻塞:
struct addrinfo hints = { .ai_flags = AI_NUMERICHOST };
getaddrinfo("192.0.2.1", "80", &hints, &res);
在异步上下文中做真正的 DNS 解析,用专门的库:
- c-ares:使用广泛,能与任何事件循环集成
- getdns:带 DNSSEC 感知,更高层的 API
避免使用 getaddrinfo_a(glibc 的异步变体)—— 它内部使用线程和信号,难以与 epoll 集成。
非阻塞 connect
第 1 步:创建非阻塞 socket
int fd = socket(res->ai_family,
res->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC,
res->ai_protocol);
if (fd == -1) { perror("socket"); return -1; }
SOCK_NONBLOCK 在创建时就把 socket 设为非阻塞,免去后续 fcntl 调用。SOCK_CLOEXEC 防止 fd 在 fork/exec 后泄漏到子进程。
第 2 步:调用 connect
retry:
int rc = connect(fd, res->ai_addr, res->ai_addrlen);
if (rc == -1 && errno != EINPROGRESS) {
if (errno == EINTR) goto retry;
perror("connect");
return -1;
}
// rc == 0:立即连接成功(比如环回)
// errno == EINPROGRESS:连接进行中
第 3 步:检测完成
当事件循环报告这个 fd 可写(EPOLLOUT)或出错(EPOLLERR、EPOLLHUP),检查结果:
int err;
socklen_t errlen = sizeof(err);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
perror("getsockopt"); return -1;
}
if (err != 0) {
errno = err;
perror("connect"); return -1;
}
// 连接已建立
非阻塞 accept
准备
int lfd = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
listen(lfd, 128);
accept 循环
当 epoll 报告监听 socket 可读时,把它排空:
for (;;) {
int fd = accept4(lfd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
if (errno == EINTR || errno == ECONNABORTED) continue;
perror("accept4"); return -1;
}
// 处理 fd
}
accept4 是 Linux 扩展,原子地在被 accept 的 socket 上设置标志,避免 accept 与 fcntl 之间的竞态。ECONNABORTED 发生在客户端在 accept 返回前重置连接时,可以安全忽略。
Nagle 算法与延迟 ACK 的交互
TCP 的 Nagle 算法会把小写入缓冲起来再一起发送,减少包数量。TCP 的延迟 ACK 机制在收到数据后最多等 40 ms 才确认,以便应用能把响应搭在一起。两者叠加会让交互式请求-响应模式出现最长 40 ms 的延迟 —— 尤其当服务器把请求的最后一个字节放在单独的 write 调用里时。
禁用 Nagle(TCP_NODELAY)
对 RPC 式或低延迟协议:
int opt = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
注意:如果你的应用在紧循环里用单字节 payload 调 write,关掉 Nagle 会让网络被微小包淹没。请改用 writev 或 sendmmsg 批量写。
禁用延迟 ACK(TCP_QUICKACK)
对”发请求、收大响应”的客户端:
int opt = 1;
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &opt, sizeof(opt));
TCP_QUICKACK 是 Linux 扩展,而且不是粘性的 —— 内核可能重置它。如果需要行为一致,在每次 recv 之后再设一次。
实现低延迟
内核旁路(kernel bypass)
想要最低的延迟(个位数微秒),完全绕开内核网络栈:
- DPDK:通用的内核旁路
- OpenOnload(Solarflare):用 LD_PRELOAD shim 做透明加速
- Mellanox VMA:面向 Mellanox/NVIDIA 网卡,类似 OpenOnload
- Exablaze:基于 FPGA,亚微秒延迟
忙轮询(仍走内核路径)
Linux 3.11 添加了 SO_BUSY_POLL,在 socket 调用内部轮询网卡驱动,而不是等中断:
int usecs = 10000;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &usecs, sizeof(usecs));
用户态忙轮询
想在不启用内核旁路的前提下把内核路径延迟压到最低,在用户态自旋:
retry:
ssize_t n = recv(fd, buf, len, MSG_DONTWAIT);
if (n == -1) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
goto retry;
perror("recv"); return -1;
}
这会烧掉一个 CPU 核,但能消除中断和调度带来的延迟。
关闭中断合并
网卡固件默认会为提升吞吐而把中断批处理。对延迟敏感的应用,关掉它:
ethtool -C eth0 adaptive-rx off rx-usecs 0 rx-frames 0
包时间戳
Linux 通过 SO_TIMESTAMPING 支持对收发包的硬件和软件时间戳。应用可以用它们来测量单向延迟(需要同步的参考时钟)、检测调度抖动、或审计网络路径。完整 API 见 Linux 内核文档中关于网络时间戳的部分。