Table of contents
Open Table of contents
TL;DR
socket API 的本质是把”可寻址的双向通信”塞进 fd 抽象(everything is a file),代价是必须通过 setsockopt/shutdown/getsockopt 等元操作补齐 TCP 状态机的语义;生产代码的复杂度集中在短读/短写循环、EAGAIN/EINTR 重试、SIGPIPE 屏蔽、字节序转换、TIME_WAIT 与 SO_REUSEADDR 的误解这五个点上。
1. Why:socket 解决了什么问题
4.2BSD(1983)之前 Unix 的 IPC 是碎片化的:pipe 只能亲缘进程、FIFO 单向无跨机、SysV IPC 臃肿、ARPANET NCP 协议特定。没有统一的、可寻址、双向、可跨机的通信 API。
socket 通过三层解耦消除了这个碎片化:
- 地址族(domain)与协议解耦 —
AF_INET / AF_INET6 / AF_UNIX / AF_PACKET共用一套socket/bind/listen/accept/connect/send/recv - 通信模式与协议解耦 —
SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET / SOCK_RAW成为正交维度 - endpoint 变成 fd — 可被
read/write/close/dup/select/poll/epoll统一处理
TCP socket vs UDP socket 的 API 本质差异
| 维度 | SOCK_STREAM (TCP) | SOCK_DGRAM (UDP) |
|---|---|---|
| 建立连接 | 必须 connect() 或 listen/accept | 可选(仅为固定 peer) |
| 数据边界 | 无;recv(n) 返回 [1, n] 任意长度 | 有;一次 recvfrom = 一个完整数据报 |
| 可靠性 | 内核保证到达 + 顺序 + 去重 | 无,应用层负责 |
recv 返 0 | 对端 FIN(优雅关闭) | 合法:收到 0 长度数据报 |
| 关闭 | shutdown → 四次挥手 | close 直接释放 |
最本质的差异是消息边界。TCP 应用必须自己做 framing(length-prefix、delimiter、HTTP chunked),UDP 天然有 framing 但要自己处理丢包和乱序。
2. sockaddr 家族的多态设计
BSD 1983 年设计时 C 没有 void*,于是用 “tagged struct + 强制转型” 模拟多态:
// 所有 sockaddr_XX 的第一字段都是 sa_family_t —— 这是 tag
struct sockaddr { sa_family_t sa_family; char sa_data[14]; };
struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port;
struct in_addr sin_addr; unsigned char sin_zero[8]; };
struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port;
uint32_t sin6_flowinfo; struct in6_addr sin6_addr;
uint32_t sin6_scope_id; };
struct sockaddr_storage { sa_family_t ss_family; char __ss_padding[...]; };
所有 API 统一用 struct sockaddr* + socklen_t,调用方先填具体结构再 reinterpret_cast。
sockaddr_storage 的用途:accept / recvfrom 时你不知道对端是 v4 还是 v6,这个类型保证对齐和大小能容纳任意地址族。生产代码里接收端用它,发送端用具体 sockaddr_in / sockaddr_in6。
为什么不用 union:BSD 尝试过但放弃——union 要求所有成员在同一翻译单元可见,会让 <sys/socket.h> 耦合所有协议头。“tag + 长度自描述” 换来了协议头解耦和 ABI 扩展性(加 IPv6 无需改 <sys/socket.h>),代价是失去类型安全。
3. 字节序:network byte order = big-endian
转换函数(<arpa/inet.h>):
| 函数 | 宽度 | 用途 |
|---|---|---|
htons / ntohs | 16-bit | 端口号 |
htonl / ntohl | 32-bit | IPv4 地址常量(INADDR_ANY、INADDR_LOOPBACK) |
htobe64 / be64toh | 64-bit | 自定义协议 payload |
黄金规则:
- 端口
sin_port—— 必须htons inet_pton输出已经是 network order,不要再转- IPv6 地址 16 字节天然就是 network order
- 大端机上所有
htonX是 no-op,但必须照写(跨架构)
4. 核心 API 精确语义
所有行为描述以 man7.org 当前版本为准,Linux 6.x 基准。
4.1 socket / accept4 的现代创建法
// 推荐:一步到位原子设置 NONBLOCK + CLOEXEC(Linux 2.6.27+)
int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
int c = ::accept4(listen_fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
必须用 SOCK_CLOEXEC:多线程程序中,A 线程 socket() 与后续 fcntl(F_SETFD, FD_CLOEXEC) 之间存在窗口,B 线程可能 fork+execve 导致 fd 泄漏到子进程。原子设置消除这个竞态。
accept 不继承 O_NONBLOCK(man accept(2) 原文警告):即使 listening socket 是非阻塞,accept 返回的新 socket 默认是阻塞的。这是与 BSD 不同的 Linux 行为。用 accept4 原子设置能避免事后 fcntl 的竞态 + 额外 syscall。
4.2 listen 的 backlog 真实语义
backlog 控制的是 accept queue(全连接队列),不是 SYN queue:
| 队列 | 状态 | 上限控制 |
|---|---|---|
| SYN queue | 收到 SYN 未完成三次握手 | /proc/sys/net/ipv4/tcp_max_syn_backlog |
| accept queue | 三次握手完,等 accept 取走 | listen(fd, backlog),但会被 /proc/sys/net/core/somaxconn 静默截断 |
Linux 5.4+ somaxconn 默认 4096(之前是 128,commit 19f92a030ca6)。
accept queue 溢出的行为(net.ipv4.tcp_abort_on_overflow=0 默认):新 SYN 被默默丢弃,依赖客户端 SYN 重传。表现:client connect() 看似成功但发数据后 server 无响应——监控上需要看 ss -lnt 的 Send-Q 列或 nstat | grep ListenOverflow。
4.3 非阻塞 connect 的正确流程
阻塞 connect 默认 SYN 重试 6 次,总耗时约 127 秒才返回 ETIMEDOUT,会把业务线程拖死。非阻塞 connect 的标准流程:
connect()返回 -1 +errno == EINPROGRESS→ 进行中poll/epoll监听POLLOUT并带超时POLLOUT触发后必须getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)读真实结果err == 0真的连上了err != 0连接失败,err是实际错误码
为什么必须用 SO_ERROR:connect() 已经返回过了不能再调;POLLOUT 只告诉你”可写”不区分”握手成功的可写”和”RST 后关闭的可写”。只有 SO_ERROR 能区分。
4.4 send / recv 返回值语义
send返回实际发送字节数,可能 < 请求len(短写)recv返回:> 0:收到字节数== 0:对端 FIN,stream socket 优雅关闭(UDP 下是合法的零长数据报)< 0:错误,查errno
短写短读是 TCP 的常态,不是 bug。正确代码必须循环到发完/收够,或遇 EAGAIN 切到事件驱动。
关键 MSG_ flags:
| Flag | 方向 | 用途 |
|---|---|---|
MSG_NOSIGNAL | send | 对端关闭时返 EPIPE 而不杀进程(生产必用) |
MSG_DONTWAIT | 双向 | 本次调用非阻塞,不改 socket 全局标志 |
MSG_PEEK | recv | 不从内核缓冲区移除,用于协议 sniff |
MSG_WAITALL | recv | 尽量凑够 len 才返回(仍可能被信号打断) |
MSG_MORE | send | 累积发送,类似 TCP_CORK |
4.5 shutdown vs close
| 维度 | shutdown(fd, how) | close(fd) |
|---|---|---|
| 作用对象 | TCP 状态机(直接操作连接) | fd 引用计数 |
| 多进程共享 fd 时 | 所有共享者都看到连接关闭 | 只减 1;引用到 0 才真正关 |
| 半关闭支持 | SHUT_RD / SHUT_WR / SHUT_RDWR | 总是全关 |
| 数据丢失风险 | 无(SHUT_WR 发 FIN,仍可读 ACK 的数据) | 有(见下) |
close 的数据丢失:默认是”后台异步发送剩余数据 + FIN”。进程随即 _exit 或内核资源紧张时数据可能丢失。需要可靠送达:shutdown(fd, SHUT_WR) → 循环 recv 到返 0 → close。
Linux 的 close 遇 EINTR 不能重试(man close(2)):fd 已经被释放,重试会关到可能被其他线程 reuse 的 fd。直接丢弃错误继续执行。
4.6 getaddrinfo 替代 gethostbyname
gethostbyname 返回静态 buffer 线程不安全、只支持 IPv4、API 已废弃。新代码一律 getaddrinfo:
- 线程安全
AF_UNSPEC同时返回 v4/v6- 同时解析主机名和服务名(端口)
- 返回链表,应用按序尝试
C++17 下用 std::unique_ptr<addrinfo, decltype(&::freeaddrinfo)> 做 RAII。
5. SO_* 选项速查(生产必知)
SOL_SOCKET 层
| 选项 | 类型 | 关键点 |
|---|---|---|
SO_REUSEADDR | bool | 不允许绑已有 active listener 的地址;只允许绑 TIME_WAIT 中的地址 |
SO_REUSEPORT | bool | Linux 3.9+,多 socket 绑同 (ip:port),内核按四元组 hash 负载均衡;所有 socket 必须同 UID |
SO_KEEPALIVE | bool | 只是开关,行为由下面 TCP_KEEPxxx 三参数决定 |
SO_LINGER | struct | 三态,见下 |
SO_RCVBUF/SO_SNDBUF | int | 内核自动翻倍;上限 rmem_max/wmem_max;一旦手动设 autotuning 关闭(生产建议不设) |
SO_RCVTIMEO/SO_SNDTIMEO | timeval | 对 select/poll/epoll 无效,只对阻塞 I/O |
SO_ERROR | int (只读) | 非阻塞 connect 必查 |
SO_REUSEADDR vs SO_REUSEPORT(经常搞混)
| SO_REUSEADDR | SO_REUSEPORT | |
|---|---|---|
| 版本 | BSD 以来 | Linux 3.9+ |
| 解决 | TIME_WAIT 复用 | 真正多 socket 绑同端口 + 内核负载均衡 |
| 谁能绑 | 无 active listener 时 | 都设此选项且同 UID |
| 典型场景 | 服务重启 | Nginx worker 并行 accept / DPDK |
SO_LINGER 三态
struct linger { int l_onoff; int l_linger; };
| 配置 | close 行为 |
|---|---|
{0, *}(默认) | 立即返回,内核后台发 FIN 走正常四次挥手 |
{1, N>0} | 阻塞最多 N 秒等所有数据 ACK;超时强制 RST |
{1, 0} | 立即发 RST,丢弃未发数据,跳过 TIME_WAIT |
典型误用:{1, N} 不保证对端应用层读走了数据,只保证对端 TCP ACK 了。真正可靠关闭要应用层双向确认。
{1, 0} 只用于紧急断连(安全场景、防慢连接占资源),绝对不能用在正常业务关闭——对端会得到 ECONNRESET 误以为是网络错误。
TCP 层选项(man tcp(7))
| 选项 | 默认 | 关键点 |
|---|---|---|
TCP_NODELAY | 0(Nagle 开) | 关 Nagle,小包立即发 |
TCP_KEEPIDLE | 7200 秒 | 空闲多久首次探测。默认 2 小时对生产几乎无用,必须调到 60 |
TCP_KEEPINTVL | 75 秒 | 探测间隔。生产建议 10 |
TCP_KEEPCNT | 9 次 | 探测失败多少次判定死链。生产建议 3 |
TCP_USER_TIMEOUT | 0(系统默认) | 未 ACK 数据强制关的总超时(ms),比 keepalive 更直接 |
TCP_DEFER_ACCEPT | 0 | listen 上设置后,accept 只在收到首个数据包时返回 |
TCP_FASTOPEN | 0 | Linux 3.6/3.7+,现实中中间盒破坏导致采用率低 |
Nagle vs delayed ACK 互相加害:Nagle 说”小包攒着”,delayed ACK 说”ACK 攒着”,两端互等产生 40ms 延迟。交互式协议一律 TCP_NODELAY=1。
IPV6_V6ONLY 的跨平台陷阱
- Linux 默认 0:IPv6 listening socket 同时接受 IPv4 连接(v4-mapped 地址
::ffff:a.b.c.d) - Windows / FreeBSD / OpenBSD 默认 1:相反
跨平台代码必须显式 setsockopt(IPV6_V6ONLY, ...),永远不依赖平台默认。
6. 错误码语义与处理策略
| errno | 含义 | 严重性 | 恢复策略 |
|---|---|---|---|
EAGAIN / EWOULDBLOCK | 非阻塞暂无法完成 / 达 SO_xxxTIMEO | 非错误 | 事件驱动等下次就绪 |
EINTR | 被信号打断 | 非错误 | while 循环重试 |
ECONNRESET | 对端 RST(崩溃 / 强关 / SO_LINGER={1,0}) | 连接死 | 关 fd |
EPIPE | stream 对端已关闭后 send | 连接死 | 关 fd;默认同时 SIGPIPE 杀进程 |
ECONNREFUSED | connect 目标端口无监听 | 失败 | 可重试 |
ECONNABORTED | accept 时 client 立即 RST | 单连接错 | continue accept 循环 |
ETIMEDOUT | SYN 重传耗尽 | 严重 | 检查网络 |
EADDRINUSE | bind 端口被占 / 4-tuple 冲突 | 严重 | SO_REUSEADDR;查 TIME_WAIT |
EMFILE / ENFILE | fd 耗尽 | 极严重 | ulimit -n;预留占位 fd |
ENOBUFS | 内核缓冲耗尽 | 严重 | 降发送速率 |
EAGAIN 与 EWOULDBLOCK 在 Linux 上是同一个值(通常 11),但 POSIX 不保证。可移植写法:if (errno == EAGAIN || errno == EWOULDBLOCK)。
EINTR 无论是否 SA_RESTART 都应循环重试(带 SO_xxxTIMEO 的 I/O 永远不重启,总是返 EINTR):
ssize_t n;
do { n = ::recv(fd, buf, len, 0); } while (n == -1 && errno == EINTR);
EMFILE 的经典应对模式
accept 循环里遇 EMFILE 会忙转 CPU。解决:启动时预打开 /dev/null 占住一个 fd;EMFILE 时 close 占位 → accept 新连接 → 立即 close 新连接(让 client 重试)→ 重开占位 fd。
7. C++17 可运行代码示例
7.1 阻塞 echo server(最小可用)
编译:g++ -std=c++17 -Wall -Wextra -O2 echo_server.cpp -o echo_server
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <csignal>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <system_error>
[[noreturn]] static void throw_errno(const char* ctx) {
throw std::system_error(errno, std::system_category(), ctx);
}
int main(int argc, char* argv[]) {
std::signal(SIGPIPE, SIG_IGN); // 必须:否则 send 到已关连接会杀进程
const uint16_t port = (argc > 1) ? static_cast<uint16_t>(std::stoi(argv[1])) : 9000;
int listen_fd = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (listen_fd < 0) throw_errno("socket");
int yes = 1;
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(port); // 字节序
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (::bind(listen_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
throw_errno("bind");
if (::listen(listen_fd, 128) < 0) throw_errno("listen");
for (;;) {
sockaddr_storage peer{};
socklen_t peer_len = sizeof(peer);
int conn = ::accept4(listen_fd, reinterpret_cast<sockaddr*>(&peer),
&peer_len, SOCK_CLOEXEC);
if (conn < 0) {
if (errno == EINTR || errno == ECONNABORTED) continue;
if (errno == EMFILE || errno == ENFILE) { std::perror("fd exhausted"); continue; }
throw_errno("accept4");
}
char buf[4096];
for (;;) {
ssize_t n;
do { n = ::recv(conn, buf, sizeof(buf), 0); } while (n < 0 && errno == EINTR);
if (n == 0) break; // 对端优雅关闭
if (n < 0) { std::perror("recv"); break; }
ssize_t sent = 0;
while (sent < n) {
ssize_t w = ::send(conn, buf + sent, n - sent, MSG_NOSIGNAL);
if (w < 0) { if (errno == EINTR) continue; std::perror("send"); goto drop; }
sent += w;
}
}
drop:
::close(conn);
}
}
7.2 非阻塞 connect + 超时
编译:g++ -std=c++17 -Wall -Wextra -O2 connect_timeout.cpp -o connect_timeout
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <variant>
struct Ok { int fd; };
struct Err { int errnum; const char* where; };
using ConnectResult = std::variant<Ok, Err>; // C++17 无 std::expected
static ConnectResult connect_with_timeout(const char* ip, uint16_t port, int timeout_ms) {
int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) return Err{errno, "socket"};
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (::inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
::close(fd); return Err{EINVAL, "inet_pton"};
}
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0) return Ok{fd};
if (errno != EINPROGRESS) { int e = errno; ::close(fd); return Err{e, "connect"}; }
pollfd pfd{fd, POLLOUT, 0};
int pr;
do { pr = ::poll(&pfd, 1, timeout_ms); } while (pr < 0 && errno == EINTR);
if (pr == 0) { ::close(fd); return Err{ETIMEDOUT, "poll timeout"}; }
if (pr < 0) { int e = errno; ::close(fd); return Err{e, "poll"}; }
int sock_err = 0;
socklen_t len = sizeof(sock_err);
if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &sock_err, &len) < 0) {
int e = errno; ::close(fd); return Err{e, "getsockopt"};
}
if (sock_err != 0) { ::close(fd); return Err{sock_err, "async connect"}; }
return Ok{fd};
}
7.3 getaddrinfo 双栈 + RAII
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
using AddrInfoPtr = std::unique_ptr<addrinfo, decltype(&::freeaddrinfo)>;
AddrInfoPtr resolve(std::string_view host, std::string_view port,
int socktype, bool passive) {
addrinfo hints{};
hints.ai_family = AF_UNSPEC; // v4 + v6
hints.ai_socktype = socktype;
hints.ai_flags = AI_ADDRCONFIG | (passive ? AI_PASSIVE : 0);
addrinfo* raw = nullptr;
std::string h{host}, p{port};
int rc = ::getaddrinfo(host.empty() ? nullptr : h.c_str(), p.c_str(), &hints, &raw);
if (rc != 0) return {nullptr, &::freeaddrinfo};
return {raw, &::freeaddrinfo};
}
std::optional<int> connect_any(const AddrInfoPtr& res) {
for (addrinfo* ai = res.get(); ai; ai = ai->ai_next) {
int fd = ::socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC, ai->ai_protocol);
if (fd < 0) continue;
if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) return fd;
::close(fd);
}
return std::nullopt;
}
7.4 RAII socket 包装器
// socket_guard.hpp
#pragma once
#include <sys/socket.h>
#include <unistd.h>
#include <utility>
#include <cerrno>
#include <system_error>
class Socket {
public:
Socket() = default;
explicit Socket(int fd) noexcept : fd_(fd) {}
static Socket create(int domain, int type, int protocol = 0) {
int fd = ::socket(domain, type, protocol);
if (fd < 0) throw std::system_error(errno, std::system_category(), "socket");
return Socket{fd};
}
~Socket() noexcept { close_quiet(); }
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
Socket& operator=(Socket&& o) noexcept {
if (this != &o) { close_quiet(); fd_ = std::exchange(o.fd_, -1); }
return *this;
}
[[nodiscard]] int get() const noexcept { return fd_; }
[[nodiscard]] bool valid() const noexcept { return fd_ >= 0; }
[[nodiscard]] int release() noexcept { return std::exchange(fd_, -1); }
private:
int fd_ = -1;
void close_quiet() noexcept {
if (fd_ >= 0) {
::shutdown(fd_, SHUT_WR); // best-effort FIN
::close(fd_); // Linux: EINTR 不能重试
fd_ = -1;
}
}
};
8. Pitfalls(生产环境 12 大坑)
8.1 TIME_WAIT 与 SO_REUSEADDR 的误解
坑:服务重启 bind 返 EADDRINUSE,以为设了 SO_REUSEADDR 就万事大吉。
真相:SO_REUSEADDR 只允许绑 TIME_WAIT 中的地址,不允许绑有 active listener 的地址。要多进程绑同端口用 SO_REUSEPORT。
8.2 SIGPIPE 默认杀进程
对已关闭连接 send,默认 SIGPIPE → 进程死。三重防御:signal(SIGPIPE, SIG_IGN) + 每次 send 用 MSG_NOSIGNAL + pthread 线程级屏蔽。
8.3 短写:send 返回值 != 请求长度
必须包装 send_all() 循环到发完或遇 EAGAIN。
8.4 recv 返 0 vs < 0 混淆
== 0 是对端 FIN(优雅关闭),不是错误。只判 < 0 会死循环。
8.5 close 后对端可能收不到已发数据
close 默认后台异步发剩余数据 + FIN,进程立即退出或资源紧张时数据可能丢。可靠关闭:shutdown(SHUT_WR) → 循环 recv 到返 0 → close。不要用 SO_LINGER={1,N} 代替应用层确认。
8.6 accept 队列溢出
server 慢 accept 填满 accept queue,新 SYN 被默默丢弃。client 表现为 connect 成功但发数据后 server 无响应。监控 ss -lnt 的 Send-Q 和 nstat | grep ListenOverflow。
8.7 字节序错误
addr.sin_port = 8080; 没 htons,实际连到 0x901f = 36895 端口。端口永远 htons;inet_pton 输出已是 network order 不要再转。
8.8 用 gethostbyname 代替 getaddrinfo
线程不安全 + 只 v4 + API 已废弃。新代码一律 getaddrinfo。
8.9 阻塞 connect 无超时
内核 SYN 重试 6 次耗时约 127 秒。推荐非阻塞 connect + poll 超时 + SO_ERROR(见 7.2)。或用 TCP_USER_TIMEOUT。SO_SNDTIMEO 在 Linux 上不作用于 connect。
8.10 fd 泄漏到子进程
socket() + fork() + 再起新程序时会继承所有 fd。一律用 SOCK_CLOEXEC / accept4(SOCK_CLOEXEC)。
8.11 IPv6 双栈行为跨平台不一致
Linux 默认 IPV6_V6ONLY=0(双栈),Windows/FreeBSD/OpenBSD 默认 1。必须显式 setsockopt,不依赖平台默认。
8.12 Linux close 遇 EINTR 不能重试
fd 已释放,重试会关到可能被其他线程 reuse 的 fd。直接吞错误继续。
9. 生产环境 Checklist
Listening socket
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); // 多 worker 负载均衡
int v6only = 1; // 或 0,显式设定
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
已建立连接
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)); // 关 Nagle
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
int idle=60, intvl=10, cnt=3;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
unsigned int to = 30000;
setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &to, sizeof(to)); // 总超时
内核参数(sysctl)
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_syncookies = 1
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.ip_local_port_range = 10000 65535
资源限制
ulimit -n 1048576 # 或 systemd: LimitNOFILE=1048576
信号
std::signal(SIGPIPE, SIG_IGN); // 进程启动最前面
观测
ss -lntp # Send-Q = 当前 accept queue 深度
nstat | grep -iE 'Overflow|Drops'
参考
所有 man page 行为以 man7.org 当前版本为准:
- socket(2) / socket(7)
- listen(2) / accept(2)
- connect(2)
- send(2) / recv(2)
- tcp(7) / ipv6(7)
- getaddrinfo(3)
I/O 多路复用相关见 epoll 使用手册。