TCP/IP Essentials for Backend Engineers
Three-way handshake,滑动窗口, TIME_WAIT, CLOSE_WAIT, half-open connections and the tools to see them all.
Handshake and teardown
TCP is connection-oriented and reliable — 'connection' here just means both sides keep per-connection state (sequence numbers, buffers, timers). Establishing it costs 3 packets, closing it costs 4.
客户端 服务端
│ SYN seq=x → │ ①三次握手
│ ← SYN+ACK seq=y,ack=x+1 │
│ ACK ack=y+1 → │ 连接建立
│ │
│ GET / HTTP/1.1 → │ 数据传输
│ ← HTTP/1.1 200 OK │
│ │
│ FIN seq=u → │ ②四次挥手
│ ← ACK ack=u+1 │
│ ← FIN seq=v │
│ ACK ack=v+1 → │ 服务端先进入 TIME_WAIT?
│ │
CLOSE_WAIT:收到 FIN、还没 close() │ FIN_WAIT_2
TIME_WAIT:主动关闭方等 2MSL │
- TIME_WAIT stays 2xMSL (~60s) on the side that actively closes, to soak up delayed/duplicate packets of the old connection
- CLOSE_WAIT is YOUR fault: the peer closed but your code hasn't called close() — find the socket leak
- Servers should let clients close first, or burn TIME_WAIT slots when they close first
A server under TIME_WAIT pressure: net.ipv4.tcp_tw_reuse=1 lets a NEW connection reuse a TIME_WAIT local port safely (timestamp-protected). Do NOT set tcp_tw_recycle (removed in 4.12) — it breaks clients behind NAT.
Reliability machinery: sequence, window, congestion
Every byte gets a sequence number; the receiver advertises a sliding window of how much buffer it has left; senders additionally respect a congestion window that grows slowly (slow start → congestion avoidance) and collapses on loss.
| Mechanism | Protects against |
|---|---|
| 序列号 + 校验和 | 乱序、损坏 |
| 重传定时器 (RTO) | 丢包(超时重传,退避加倍) |
| 快重传 + 快恢复 | 连续 3 个重复 ACK 就不等超时 |
| 滑动窗口 (rwnd) | 接收方处理不过来 |
| 拥塞窗口 (cwnd) | 网络中间链路拥塞 |
# 查看连接状态分布(经典排障命令)
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
# 查看内核 TCP 参数
sysctl net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range \
net.ipv4.tcp_max_tw_buckets net.core.somaxconn
For a reverse proxy the classic exhaustion story: 60k outbound ephemeral ports × 60s TIME_WAIT ≈ 1000 new connections/sec per (src_ip, dst_ip, dst_port) tuple. The fix is longer-lived upstream connections (keepalive pool), not sysctl tricks.
Application-level habits that matter
- Set SO_KEEPALIVE (or app-layer heartbeats): a silently dead peer can leave a half-open socket forever otherwise
- Set reasonable read timeouts: a slow-loris client can pin your worker threads
- Graceful shutdown = stop accepting, drain in-flight, then close: this keeps you out of the 'connection reset' race
# Go:同时设心跳、读超时与优雅停机
ln, _ := net.Listen("tcp", ":8080")
srv := &http.Server{
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go srv.Serve(ln)
<-stop // 收到 SIGTERM
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx) // 排空在途连接再退握手与挥手
TCP 是面向连接、可靠的协议——「连接」的意义只是双方各自保存一份连接状态(序列号、缓冲区、定时器)。建立要 3 个包,关闭要 4 个包。
客户端 服务端
│ SYN seq=x → │ ①三次握手
│ ← SYN+ACK seq=y,ack=x+1 │
│ ACK ack=y+1 → │ 连接建立
│ │
│ GET / HTTP/1.1 → │ 数据传输
│ ← HTTP/1.1 200 OK │
│ │
│ FIN seq=u → │ ②四次挥手
│ ← ACK ack=u+1 │
│ ← FIN seq=v │
│ ACK ack=v+1 → │ 服务端先进入 TIME_WAIT?
│ │
CLOSE_WAIT:收到 FIN、还没 close() │ FIN_WAIT_2
TIME_WAIT:主动关闭方等 2MSL │
- TIME_WAIT 停在主动关闭方,约 2MSL(约60秒),用来兜底旧连接的迟到/重复包
- 出现 CLOSE_WAIT 是你的问题:对方关了你没 close()——查 socket 泄漏
- 尽量让客户端先关闭连接,服务端主动关闭会积累 TIME_WAIT
TIME_WAIT 压力大时把 net.ipv4.tcp_tw_reuse=1,新连接可安全复用 TIME_WAIT 的本地端口(有时间戳保护)。千万别开 tcp_tw_recycle(4.12 已移除)——它会搞坏 NAT 后面的客户端。
可靠性的机器:序列号、滑动窗口、拥塞控制
每个字节都有序列号;接收方通告「我还剩多少缓冲」(滑动窗口);发送方还额外遵守一个慢慢增长的拥塞窗口(慢启动 → 拥塞避免),丢包时立刻收缩。
| 机制 | 防范什么 |
|---|---|
| 序列号 + 校验和 | 乱序、损坏 |
| 重传定时器 (RTO) | 丢包(超时重传,退避加倍) |
| 快重传 + 快恢复 | 连续 3 个重复 ACK 就不等超时 |
| 滑动窗口 (rwnd) | 接收方处理不过来 |
| 拥塞窗口 (cwnd) | 网络中间链路拥塞 |
# 查看连接状态分布(经典排障命令)
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
# 查看内核 TCP 参数
sysctl net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range \
net.ipv4.tcp_max_tw_buckets net.core.somaxconn
反向代理的经典耗尽故事:6 万个临时端口 × 60 秒 TIME_WAIT ≈ 每个(源IP,目的IP,目的端口)组合每秒约 1000 个新连接。解法是上游长连接(连接池),而不是调内核参数。
应用层该有的习惯
- 设 SO_KEEPALIVE(或应用层心跳):否则对端悄悄死了会留下永久半开连接
- 设合理的读超时:慢速攻击客户端能占死你的工作线程
- 优雅停机 = 停止 accept → 处理完在途请求 → 再关闭:避免出现「连接被重置」的竞态
# Go:同时设心跳、读超时与优雅停机
ln, _ := net.Listen("tcp", ":8080")
srv := &http.Server{
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go srv.Serve(ln)
<-stop // 收到 SIGTERM
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx) // 排空在途连接再退