TCP 行为¶
是什么 / 解决什么问题¶
理解 TCP 协议栈的运行时行为,帮助诊断网络性能问题、连接异常、延迟抖动等。重点不在协议规范,而在"内核 TCP 实现实际会怎么做"。
连接生命周期¶
三次握手¶
客户端 服务端
│ │
│──── SYN ──────────────►│ connect() 发起
│ │ (进入 SYN 队列)
│◄─── SYN+ACK ──────────│
│ │
│──── ACK ──────────────►│ (移入 Accept 队列)
│ │ accept() 取走
│ 连接建立 │
四次挥手¶
主动关闭方 被动关闭方
│ │
│──── FIN ──────────────►│ close()/shutdown(WR)
│ │ read() 返回 0
│◄─── ACK ──────────────│
│ │
│◄─── FIN ──────────────│ close()
│ │
│──── ACK ──────────────►│
│ │
│ TIME_WAIT (2*MSL) │ CLOSED
│ 60s 后 │
│ CLOSED │
连接状态转换的实际影响¶
| 状态 | 含义 | 常见问题 |
|---|---|---|
| SYN_SENT | 等待 SYN+ACK | connect 超时 |
| SYN_RECV | 收到 SYN,等 ACK | SYN flood 攻击填满队列 |
| ESTABLISHED | 正常数据传输 | — |
| FIN_WAIT_1 | 发了 FIN,等 ACK | 对端不响应 |
| FIN_WAIT_2 | 收到 ACK,等对端 FIN | 对端 close 泄漏 |
| TIME_WAIT | 主动关闭后等待 2MSL | 端口耗尽 |
| CLOSE_WAIT | 收到 FIN,等应用 close | 常见 bug:fd 泄漏 |
行为与语义¶
Nagle 算法¶
目的:减少网络上小包数量。
规则:如果有已发送但未确认的数据,则缓冲新的小数据直到: 1. 累积到 MSS(通常 1460 字节) 2. 收到前一个包的 ACK
发送: write("H") write("e") write("l") write("l") write("o")
有 Nagle: [H] ──等ACK──► [ello] ──► (两个包)
无 Nagle: [H] [e] [l] [l] [o] (五个包)
问题:Nagle + Delayed ACK 组合导致 40ms 延迟
客户端 (Nagle ON) 服务端 (Delayed ACK)
│ │
│─── 小包1 ───────────────►│ 收到,等 200ms 或等凑够一个 ACK
│ 想发小包2,但 Nagle │
│ 说要等 ACK... │
│ │ (40ms Delayed ACK 定时器到)
│◄── ACK ─────────────────│
│─── 小包2 ───────────────►│
│ │
总延迟:40ms(无谓等待)
解决方案:
// 方案1:关闭 Nagle
int on = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
// 方案2:用 writev 合并多个小写入为一次 syscall
struct iovec iov[2] = {
{ .iov_base = header, .iov_len = header_len },
{ .iov_base = body, .iov_len = body_len },
};
writev(fd, iov, 2); // 内核会作为一个 TCP 段发送
// 方案3:TCP_CORK 先塞住再放开
int on = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &on, sizeof(on));
write(fd, header, header_len);
write(fd, body, body_len);
on = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &on, sizeof(on)); // flush
TIME_WAIT¶
- 持续 2*MSL(Linux 默认 60s,硬编码不可改)
- 目的:确保最后的 ACK 到达对端;防止旧连接的延迟包影响新连接
- 只有主动关闭方进入 TIME_WAIT
常见问题与解决:
// 1. SO_REUSEADDR — 允许 bind 到 TIME_WAIT 地址
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
// 2. tcp_tw_reuse — 允许复用 TIME_WAIT 连接(客户端出方向)
// sysctl net.ipv4.tcp_tw_reuse = 1
// 3. 让对端主动关闭(谁主动关闭谁承担 TIME_WAIT)
// HTTP 中 server 发送 Connection: close 让 client 先关
// 4. 连接池 — 从根本上减少连接创建/关闭
Backlog 与连接排队¶
SYN flood 保护:
/proc/sys/net/ipv4/tcp_syncookies = 1 (默认开启)
SYN 队列满时不丢弃,用 cookie 验证
Accept 队列满时行为:
/proc/sys/net/ipv4/tcp_abort_on_overflow = 0 (默认)
→ 忽略客户端 ACK,客户端重传 SYN(表现为连接建立慢)
= 1
→ 发送 RST(客户端立即收到 connection reset 错误)
# 观察 Accept 队列溢出
nstat -az | grep -i listen
# TcpExtListenOverflows: Accept 队列满次数
# TcpExtListenDrops: 因队列满丢弃的连接数
Keep-Alive¶
TCP 层的心跳机制(与 HTTP keep-alive 不同):
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
// 默认参数(极其保守):
// tcp_keepalive_time = 7200s (2小时后才开始探测)
// tcp_keepalive_intvl = 75s (每75s探测一次)
// tcp_keepalive_probes = 9 (9次无响应才判定死亡)
// 总计:2h + 75*9 = 2h11m 才能发现死连接
// 通常需要调整为更积极的值
int idle = 60; // 60s 空闲后开始探测
int intvl = 10; // 每 10s 一次
int cnt = 3; // 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));
// 总计:60 + 10*3 = 90s 发现死连接
拥塞控制¶
内核维护 cwnd(拥塞窗口),控制发送速率:
常用算法:
| 算法 | 特点 | 适用 |
|---|---|---|
| cubic | Linux 默认,适合高带宽长延迟 | 通用 |
| bbr | Google 开发,基于带宽估计而非丢包 | 高丢包网络 |
| reno | 经典,cwnd 恢复慢 | 教学 |
# 查看/设置拥塞算法
sysctl net.ipv4.tcp_congestion_control
# net.ipv4.tcp_congestion_control = cubic
# 改为 bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 也可以 per-socket 设置
setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 3);
# 查看初始拥塞窗口
ip route show | grep initcwnd
# 默认 10 (10个MSS ≈ 14.6KB)
# 调大初始窗口(适合内网短连接)
ip route change default via <gw> initcwnd 32
零窗口与窗口探测¶
接收方缓冲区满时通告零窗口:
发送方 接收方
│ │ (recv buffer 满,应用层处理慢)
│◄── ACK, win=0 ──────────│ "我满了,别发了"
│ │
│ 持续定时器:每隔一段 │
│ 时间发送窗口探测包 │
│──── 1字节探测 ──────────►│
│◄── ACK, win=0 ──────────│ "还是满的"
│ ... │
│◄── ACK, win=8192 ───────│ "有空间了"
│──── 继续发数据 ─────────►│
如果应用层 read 慢导致频繁零窗口,说明处理能力跟不上。
性能考量¶
影响吞吐的关键参数¶
# 发送/接收缓冲区(直接影响带宽延迟积)
# BDP = bandwidth * RTT
# 100Mbps * 10ms = 125KB 缓冲区才能跑满
sysctl net.ipv4.tcp_rmem # 最小 默认 最大
sysctl net.ipv4.tcp_wmem
# 默认: "4096 131072 6291456" (4K 128K 6M)
# 内网高带宽场景可以调大
sysctl -w net.ipv4.tcp_rmem="4096 524288 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 524288 16777216"
影响延迟的关键因素¶
- Nagle + Delayed ACK → 关闭 TCP_NODELAY
- 慢启动 → 连接池复用,避免新连接
- 拥塞控制 → BBR 在有丢包时表现更好
- Accept 队列满 → 增大 backlog 和 somaxconn
常见陷阱与 FAQ¶
1. CLOSE_WAIT 堆积¶
ss -tan state close-wait | wc -l
# 如果持续增长,说明应用层没有 close() 已收到 FIN 的连接
# 这是 fd 泄漏!检查代码中 read 返回 0 后是否 close
2. RST 的常见原因¶
- 连接不存在(对方已关闭或重启)
- 对方 close 时 recv buffer 中还有未读数据(强制 RST 而非 FIN)
- SO_LINGER l_onoff=1, l_linger=0(close 时发 RST)
- 防火墙丢包后超时
3. 小包性能问题¶
// 差:多次小 write
write(fd, "GET ", 4);
write(fd, path, path_len);
write(fd, " HTTP/1.1\r\n", 11);
// 好:合并后一次写
char request[4096];
int len = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n", path);
write(fd, request, len);
// 或用 writev
struct iovec iov[3] = { ... };
writev(fd, iov, 3);
观测与调试¶
# TCP 连接详细信息
ss -tnpi dst :8080
# 输出含:cwnd, rtt, retrans, send buffer 等
# 查看重传
nstat -az | grep -i retrans
# TcpRetransSegs: 重传段数
# 查看 TCP 内存使用
cat /proc/net/sockstat
# TCP: inuse 1234 orphan 5 tw 678 alloc 1300 mem 890
# tcpdump 抓包分析
tcpdump -i eth0 -nn port 8080 -w dump.pcap
# 用 wireshark 打开分析
# 查看某连接的拥塞窗口变化
ss -ti dst 10.0.0.1:8080
# cubic wscale:7,7 rto:204 rtt:1.5/0.5 cwnd:10 ssthresh:7
延伸阅读¶
man 7 tcp— TCP socket 选项和行为- RFC 9293 — TCP 规范
sysctl net.ipv4.tcp_*— 内核 TCP 参数一览- Brendan Gregg 的 TCP/IP 性能分析文章