TCP-IP协议栈与抓包分析
1. 概念与原理
TCP/IP 是互联网的事实标准,前端的每个 fetch、每张图片、每次 WebSocket 都跑在它上面。理解它,你才能解释为什么 Connection reset 不是超时、为什么大文件下载偶尔卡 30 秒、为什么 keep-alive 能省一半建连开销。
1.1 OSI 七层 vs TCP/IP 四层
| OSI | TCP/IP | 协议示例 | 前端关心 |
|---|---|---|---|
| 应用层 | 应用层 | HTTP、WebSocket、DNS | ★★★★★ |
| 表示层 | (并入应用层) | TLS、JSON | ★★★★ |
| 会话层 | (并入应用层) | TLS Session | ★★ |
| 传输层 | 传输层 | TCP、UDP、QUIC | ★★★★ |
| 网络层 | 网际层 | IP、ICMP | ★★★ |
| 数据链路层 | 网络接口 | Ethernet、ARP | ★ |
| 物理层 | 网络接口 | 网线、光纤 | - |
每一层都给上层数据加 header,下层只看自己的 header。这叫 封装(encapsulation):
[Ethernet][IP][TCP][HTTP payload]
tcpdump -X 看到的就是这个完整的字节序列。
1.2 IP 协议核心要点
- 无连接:每个包独立路由,可能走不同路径
- 不可靠:丢包、乱序、重复都不保证。可靠交给 TCP
- MTU:链路最大传输单元。以太网 1500 字节,包含 IP/TCP header(一般 40 字节),TCP 实际 payload(MSS)= 1460 字节
- 分片:包大于 MTU 会在路由器分片,性能差。现代用 PMTUD 探测路径 MTU
# 看路径 MTU
tracepath example.com
# 不分片探测
ping -M do -s 1472 example.com # 1472 + 28(ICMP) = 1500
MTU 不一致是经典坑。隧道(VPN、PPPoE)会减小 MTU,云内网常见 9000(jumbo frame),混用会丢大包。
2. TCP 核心机制
2.1 三次握手
Client Server
| SYN, seq=x |
|─────────────────────────────> | ① Client → Server
| |
| SYN+ACK, seq=y, ack=x+1 |
| <─────────────────────────────| ② Server → Client
| |
| ACK, ack=y+1 |
|─────────────────────────────> | ③ Client → Server
| |
| 连接建立,开始数据传输 |
为什么三次:
- 一次:服务端不知道客户端的接收能力
- 两次:客户端不知道自己发的能不能到,且第三次才能确认服务端的初始序列号
序列号(seq)不是从 0 开始,是随机的,防止旧连接的包混入新连接(也防序列号预测攻击)。
握手抓包验证:
sudo tcpdump -i any -nn -S 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and host example.com' -c 6
# 输出三次握手 + 后续 ACK
2.2 四次挥手
Client Server
| FIN, seq=u |
|───────────────────────────> | ① 主动方提出关闭
| |
| ACK, ack=u+1 |
| <───────────────────────────| ② 被动方确认
| |
| FIN, seq=v |
| <───────────────────────────| ③ 被动方关闭(可能延迟,因为可能还要发数据)
| |
| ACK, ack=v+1 |
|───────────────────────────> | ④ 主动方确认
| |
| TIME_WAIT 2*MSL |
为什么四次:TCP 是全双工,两个方向独立关闭。被动方收到 FIN 后可能还有数据要发,所以 ACK 和 FIN 分开。
2.3 TIME_WAIT 与 CLOSE_WAIT(必懂)
| 状态 | 出现在 | 含义 | 危险信号 |
|---|---|---|---|
| TIME_WAIT | 主动关闭方 | 等 2*MSL(默认 60s)确保对方收到最后 ACK + 让旧包过期 | 大量出现 = 应用主动关闭连接频繁,端口耗尽 |
| CLOSE_WAIT | 被动关闭方 | 收到对方 FIN 后还没调用 close() | 大量出现 = 应用代码 bug,没正确关连接 |
ss -tn state time-wait | wc -l
ss -tn state close-wait | wc -l
CLOSE_WAIT 堆积是 Node 应用最常见的连接泄漏,常见原因:
- HTTP client 没正确处理 response 流,没读完也没 destroy
- 数据库连接池配置不当,连接没回收
- WebSocket 没监听 close 事件
- axios 长连接 keepAlive agent 配置错误
排查:lsof -p <pid> | grep CLOSE_WAIT 看是哪些远端 IP,再去看代码对应的请求逻辑。
2.4 TIME_WAIT 优化
高并发场景,本机作为客户端发起大量短连接(如 Nginx 反向代理到上游),TIME_WAIT 堆积会耗尽本地端口(默认范围 32768-60999)。
正确做法:
- 复用连接(首选):HTTP keep-alive、连接池、长连接
- 调内核参数(生产慎用):
# /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1 # 允许 TIME_WAIT socket 给新连接(需双方支持时间戳)
net.ipv4.ip_local_port_range = 10000 65535 # 扩大端口范围
# net.ipv4.tcp_tw_recycle 在 4.12 内核已删除,不要再用(NAT 环境会丢包)
sysctl -p
tcp_tw_recycle 历史遗毒,老资料会推荐,绝对不要开,会导致 NAT 环境下连接随机失败。
2.5 重传与拥塞控制
重传
TCP 通过 ACK 确认接收,没收到 ACK 就重传。重传有两种触发:
- 超时重传(RTO):定时器到期没 ACK,等 RTO 时间。RTO 动态计算,初始 1s,指数退避
- 快速重传:收到 3 个重复 ACK 立即重传,不等 RTO
抓包看重传:
sudo tcpdump -i any -nn 'tcp' -w cap.pcap
# Wireshark 打开,过滤 tcp.analysis.retransmission
重传率 > 1% 表示网络质量差。
拥塞控制
经典四阶段:慢启动 → 拥塞避免 → 快速重传 → 快速恢复。
Linux 默认 CUBIC(高带宽适合)。Google 的 BBR 算法在弱网(高延迟、丢包)下显著好于 CUBIC:
# 看当前算法
sysctl net.ipv4.tcp_congestion_control
# 切到 BBR(4.9+ 内核)
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
# 验证
sysctl net.ipv4.tcp_available_congestion_control
前端服务器开启 BBR 是高 ROI 操作:跨地域用户、移动网络下载速度可能翻倍。
2.6 滑动窗口与 Nagle 算法
- 滑动窗口:接收方告诉发送方还能收多少(rwnd),流量控制
- 拥塞窗口(cwnd):发送方根据网络状态动态调整
- 实际窗口 = min(rwnd, cwnd)
- BDP(带宽时延积) = 带宽 × RTT。窗口必须 ≥ BDP 才能跑满带宽。100Mbps × 100ms = 1.25MB,默认 Linux rwnd 64KB 远小于这个,需要 window scaling 扩大
Nagle 算法:合并小包发送(减少 packet 数)。但和 delayed ACK 一起会引入延迟。实时应用(游戏、SSH)通常关 Nagle:
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
Node 默认关:socket.setNoDelay(true),HTTP server 自动开启。
2.7 TCP keep-alive vs HTTP keep-alive
很多人混淆:
| TCP keep-alive | HTTP keep-alive | |
|---|---|---|
| 层级 | 传输层(OS) | 应用层(HTTP header) |
| 目的 | 探测对端是否还活着 | 复用 TCP 连接发多个 HTTP 请求 |
| 默认 | 关闭,开启后 7200s 才发探测 | HTTP/1.1 默认开启 |
| 配置 | net.ipv4.tcp_keepalive_* | Connection: keep-alive + Keep-Alive 头 |
云负载均衡常配 60s 空闲断连,TCP keep-alive 默认 7200s 探测来不及,连接已被中间设备 RST。客户端要主动调小:
# 全局
net.ipv4.tcp_keepalive_time = 60 # 空闲多久开始探测
net.ipv4.tcp_keepalive_intvl = 10 # 探测间隔
net.ipv4.tcp_keepalive_probes = 3 # 几次没响应判死
或代码层面(Node):
const net = require('net')
socket.setKeepAlive(true, 60000)
3. UDP 与 QUIC
3.1 UDP
无连接、不可靠、不保序。优点:开销小(8 字节 header)、低延迟、广播组播。前端场景:
- DNS 查询(53 端口,UDP/TCP 都可)
- WebRTC 媒体流
- HTTP/3(基于 QUIC,QUIC 基于 UDP)
3.2 QUIC
Google 设计,IETF 标准化(RFC 9000)。在 UDP 上重新实现可靠传输 + TLS 1.3 + 多路复用。优势:
- 0-RTT 重连:复用 session 直接发数据
- 没有 TCP 队头阻塞:流之间独立
- 连接迁移:手机从 WiFi 切 4G,连接不断
HTTP/3 = HTTP semantics over QUIC。详见"HTTP 协议演进"。
4. 抓包实战
4.1 tcpdump 进阶
# 抓 80 端口三次握手
sudo tcpdump -i any -nn -S 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0 and port 80'
# 看 TCP 重传(序列号倒退)
sudo tcpdump -i any -nn -S 'tcp and port 443' -w cap.pcap
# Wireshark 过滤 tcp.analysis.retransmission
# 抓 RST 包(连接被强制重置)
sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-rst != 0'
# 抓某主机的全部交互
sudo tcpdump -i any -nn -A 'host api.example.com' -w cap.pcap
# 限制大小避免磁盘炸
sudo tcpdump -i any -nn -W 5 -C 100 -w cap.pcap # 最多 5 个 100MB 文件
4.2 Wireshark 关键过滤
ip.addr == 1.2.3.4
tcp.port == 443
http.response.code >= 400
tls.handshake.type == 1 # ClientHello
tcp.analysis.retransmission # 重传
tcp.analysis.duplicate_ack
tcp.flags.reset == 1
tcp.stream eq 0 # 第 0 个流的所有包
右键 → Follow → TCP Stream 看完整对话。Statistics → Conversations 看连接列表。
4.3 案例:诊断 Connection reset
# 现象:客户端报 ECONNRESET,前端报 net::ERR_CONNECTION_RESET
# 1. 服务器端抓包
sudo tcpdump -i any -nn -S host <client-ip> -w reset.pcap
# 2. Wireshark 找 RST 包,看是谁先发的
# - 服务端先发 RST:服务端主动重置(应用 abort、过载拒绝、超时)
# - 客户端先发 RST:客户端 abort(用户取消、超时)
# - 中间设备发 RST:防火墙、运营商 reset 攻击
# 3. 看 RST 之前的最后几个包,往往能看出原因
# - HTTP 415/413 后 RST:服务端拒绝大请求
# - SYN 后立即 RST + ACK:端口没监听
# - 长时间空闲后 RST:中间设备超时断连
5. 性能调优
5.1 关键内核参数(Web 服务器)
# /etc/sysctl.conf 高并发服务器调优
# === 连接队列 ===
net.core.somaxconn = 65535 # 全连接队列上限(应用 listen() backlog 不能超过这个)
net.ipv4.tcp_max_syn_backlog = 65535 # 半连接队列
# === TIME_WAIT ===
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT2 超时
# === 端口范围 ===
net.ipv4.ip_local_port_range = 10000 65535
# === keepalive ===
net.ipv4.tcp_keepalive_time = 600
# === 缓冲 ===
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# === 拥塞控制 ===
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# === SYN 防护 ===
net.ipv4.tcp_syncookies = 1 # 防 SYN flood
sysctl -p
重要:somaxconn 是上限,应用 listen(fd, backlog) 还要在代码层调大。Nginx 默认 backlog 511,K8s 里很多人忘记调。
5.2 Nginx 配置侧
events {
worker_connections 65535;
use epoll;
}
http {
keepalive_timeout 65;
keepalive_requests 1000;
upstream backend {
keepalive 100; # 到上游的连接池
}
}
6. 故障排查
6.1 连接数异常
# 总览
ss -s
# Total: 500 (kernel 0)
# TCP: 500 (estab 200, closed 100, orphaned 0, synrecv 0, timewait 200/0)
# TIME_WAIT 多
ss -tan state time-wait | head
ss -tan state time-wait | awk '{print $4}' | cut -d: -f1 | sort | uniq -c
# CLOSE_WAIT 多(一定是 bug)
ss -tan state close-wait
lsof -p <pid> | grep CLOSE_WAIT
# 半连接队列溢出(看 SYN flood 或 accept 慢)
netstat -s | grep -i "SYNs.*overflow"
netstat -s | grep -i "listen drops"
6.2 网络丢包
# 网卡级
ip -s link show eth0
# 看 RX/TX errors / dropped
# 协议级
netstat -s | grep -i "retrans\|drop"
# segments retransmitted: 1000
# 用 mtr 持续观测路径丢包
mtr -r -c 100 example.com
6.3 大文件下载偶尔卡住
经典原因:BDP 不够大 → 窗口太小 → 跑不满带宽。
# 看接收方窗口
ss -tin
# bbr cwnd:10 ssthresh:7 ... rcv_space:14600
# 优化(接收方)
sysctl -w net.core.rmem_max=16777216
sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216'
7. 安全考量
- SYN flood:开
tcp_syncookies = 1,攻击时不分配资源 - 小包攻击 / Slowloris:Nginx 的
client_body_timeout、client_header_timeout - 明文协议泄露:HTTP / 老 TLS 在抓包工具下无密。生产强制 HTTPS + TLS 1.2+
- 抓包数据敏感:pcap 文件含 cookie、token,处理后立即销毁
8. 常见反模式
- 不开 keep-alive:每次请求三次握手 + TLS 握手,性能差 5-10 倍
- 客户端短连接 + 高并发:本地端口耗尽
tcp_tw_recycle = 1:NAT 环境必死,4.12 内核已删除- 认为 ping 通就是网络好:ICMP 和 TCP 路径可能不同,且 ICMP 经常被禁
- 不调
somaxconn:QPS 上来后 SYN 队列溢出,连接无故失败 - 抓包不加过滤条件:磁盘瞬间塞满
- 看到 RST 就以为是攻击:很多正常情况会触发 RST(应用 abort、HTTP 1.0 关连接)
- 不区分 TCP keepalive 和 HTTP keepalive:调错地方解决不了问题
9. 延伸阅读
- 《TCP/IP 详解 卷一》Stevens — TCP/IP 圣经,必读
- 《Wireshark 网络分析就这么简单》林沛满 — 中文抓包入门最佳
- High Performance Browser Networking — Ilya Grigorik,免费在线版,前端必读
- Linux 内核网络协议栈源码分析 — 进阶
- Cloudflare Blog: TCP/QUIC 系列 — 实战级深度文章