逐字节拆解 tcpdump

逐字节拆解 tcpdump:从恐惧到理解

写在前面

很多人对 tcpdump 有恐惧心理------满屏十六进制和无结构文本,本能觉得"我读不懂"。但 tcpdump 本质上只是一个安静的监听器,不拦截、不修改、不丢包,纯粹旁路观察。

恐惧的根源不是工具难,是输出太"原始"。解决方法不是背命令,是逐字节拆开看

下面是一次完整 HTTP 请求的 10 行 tcpdump 输出,从三次握手到数据传输到连接关闭,每一行、每一个字段全部拆解。


实验环境

arduino 复制代码
本机:192.168.31.218(无线网卡 wlp3s0)
目标:httpbin.org → 44.216.249.42:80
命令:curl http://httpbin.org/get

抓包命令

bash 复制代码
sudo tcpdump -i wlp3s0 -c 10 -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'
  • -i wlp3s0:监听无线网卡
  • -c 10:只抓 10 个包
  • -nn:不反解 IP 和端口(永远加这个选项,反解会拖慢输出且容易误导)
  • BPF 过滤器:只看带 SYN 或 ACK 标志的包,排除 SSH 流量

完整输出

yaml 复制代码
13:58:04.102727 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [S], seq 3629117031, win 64240, options [mss 1460,sackOK,TS val 2761634164 ecr 0,nop,wscale 7], length 0
13:58:04.342796 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [S.], seq 1688432090, ack 3629117032, win 26883, options [mss 1402,nop,nop,sackOK,nop,wscale 8], length 0
13:58:04.342883 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2761634408 ecr 1688432022], length 0
13:58:04.343049 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [P.], seq 1:79, ack 1, win 502, options [nop,nop,TS val 2761634409 ecr 1688432022], length 78: HTTP: GET /get HTTP/1.1
13:58:04.612405 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [.], ack 79, win 106, length 0
13:58:04.612406 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [P.], seq 1:231, ack 79, win 106, length 230: HTTP: HTTP/1.1 200 OK
13:58:04.612408 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [P.], seq 231:486, ack 79, win 106, length 255: HTTP
13:58:04.612500 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 231, win 501, length 0
13:58:04.612536 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 486, win 501, length 0
13:58:04.612917 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [F.], seq 79, ack 486, win 501, length 0

输出结构速查

每一行的通用结构:

bash 复制代码
时间戳 协议 源IP.源端口 > 目标IP.目标端口: Flags [标志], seq, ack, win, options, length
字段 含义
时间戳 精确到微秒,内核记录
协议 IP(IPv4)或 IP6(IPv6)
源 > 目标 数据包的流向
Flags TCP 标志位:S=SYN, .=ACK, P=PSH, F=FIN, R=RST
seq 序列号(tcpdump 默认显示相对值)
ack 确认号(确认对方已收到的数据)
win 接收窗口大小
options TCP 选项(握手时协商参数)
length TCP 载荷长度 = 应用层数据量,不含任何头部

第 1 行:SYN------连接请求

ini 复制代码
13:58:04.102727 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [S], seq 3629117031, win 64240, options [mss 1460,sackOK,TS val 2761634164 ecr 0,nop,wscale 7], length 0

本机向 httpbin.org 发起连接。58846 是内核随机分配的临时端口,80 是 HTTP 默认端口。

Flags [S] = SYN,"我想建立连接"。这是三次握手的第一步。

seq 3629117031 = 初始序列号(ISN),由内核随机生成,不从 0 或 1 开始------这是安全设计,防止攻击者猜到序列号伪造包。tcpdump 会把这个值记为基准 0,后续所有 seq/ack 都减去基准来显示相对值。

win 64240 = 接收窗口,"我的缓冲区能容纳 64240 字节"。

options 里是连接参数的"合同",只在 SYN 包里协商,一旦建立连接就不能改:

选项 含义
mss 1460 单个 TCP 段最大载荷 1460 字节(以太网 MTU 1500 - 20 IP头 - 20 TCP头)
sackOK 支持选择性确认,丢了只重传丢失的段,不用全部重传
TS val 2761634164 ecr 0 时间戳,ecr=0 因为是第一个包,还没收到过对方的时间戳
nop 填充字节,让后面的选项对齐到 4 字节边界
wscale 7 窗口缩放因子,实际窗口 = win × 2^7 = 约 8.2MB

length 0 = SYN 不带应用数据,纯粹是握手信号。


第 2 行:SYN-ACK------服务器回应

ini 复制代码
13:58:04.342796 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [S.], seq 1688432090, ack 3629117032, win 26883, options [mss 1402,nop,nop,sackOK,nop,wscale 8], length 0

方向反转:服务器 → 你。和时间戳差 240ms,这就是这段链路的 RTT。

Flags [S.] = SYN + ACK 同时置位。一个包完成两件事:确认你的 SYN + 发起自己方向的连接。这就是为什么三次握手能少一步------SYN 和 ACK 合并成了一个包。

seq 1688432090 = 服务器的 ISN,和你客户端的 ISN 没有任何数学关系,独立随机生成。TCP 是全双工的,两个方向各有独立的序列号空间。

ack 3629117032 = 你的 ISN + 1。SYN 占 1 个序列号(即使 length 0),所以确认号 = 收到的 seq + 1。

mss 1402 比你的 1460 小 58 字节------AWS 内部用了 VXLAN 封装,多了几十字节外层头。实际传输取双方 MSS 的较小值,所以这个连接的单段上限是 1402。

wscale 8 比你的 7 大,双方可以不同,各自声明各自的。实际窗口 = 26883 × 2^8 ≈ 6.9MB。


第 3 行:ACK------握手完成

ini 复制代码
13:58:04.342883 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 2761634408 ecr 1688432022], length 0

和第 2 行相差 87 微秒------内核网络栈几乎瞬间完成处理。

Flags [.] = 纯 ACK。三次握手到此完成,连接建立。

ack 1 是相对值。tcpdump 为两个方向各维护一个基准:seq 跟自己方向的 ISN 比,ack 跟对方方向的 ISN 比。所以这里 ack 1 = 服务器 ISN + 1 的相对表示。

win 502 看起来比第 1 行的 64240 小很多,但这是缩放后的值:502 × 2^7 = 64256,和 64240 接近。握手完成后 wscale 生效,tcpdump 已经帮你做了缩放。

第三次握手的 ACK 其实可以带数据------RFC 793 明确允许。如果 curl 提前准备好了请求,完全可以 ACK + PSH + 数据合并成一个包,省掉一次网络传输。只是通常应用层来不及那么快,所以先发纯 ACK。


第 4 行:HTTP 请求发出

bash 复制代码
13:58:04.343049 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [P.], seq 1:79, ack 1, win 502, options [nop,nop,TS val 2761634409 ecr 1688432022], length 78: HTTP: GET /get HTTP/1.1

握手完成 166 微秒后,应用层就把 HTTP 请求塞进去了。

Flags [P.] = PSH + ACK。PSH 不是"我有数据"的意思(有数据自然会有 length),而是告诉接收方"数据到了,别攒着,立刻交给应用层"。

seq 1:79 是冒号表示法,左闭右开区间:字节 1 到 78,共 78 字节。和 length 78 吻合。这种表示法一眼就能看出这个包带了多少数据------排障时看 seq 和 ack 的差值就能判断数据有没有完整到达对端。

ack 1 和第 3 行一样------服务器还没发新数据,ack 不变。ack 只追踪对方发了多少数据,跟自己是第几个包无关。

HTTP: GET /get HTTP/1.1 是 tcpdump 做的应用层解码,自动识别常见协议。78 字节不只是请求行,还包含所有 HTTP 头(Host、User-Agent 等)。


第 5 行:服务器确认收到请求

ini 复制代码
13:58:04.612405 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [.], ack 79, win 106, length 0

和第 4 行相差 269ms,接近 RTT。

ack 79 = 1 + 78,确认你的 78 字节请求全部收到。ack 的跳变是排障的"进度条"------ack 没变说明对方没收到新数据,ack 跳到预期值说明数据全部到达。

没有 options,纯 ACK 包省略了时间戳选项以减少开销。


第 6 行:HTTP 响应头

yaml 复制代码
13:58:04.612406 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [P.], seq 1:231, ack 79, win 106, length 230: HTTP: HTTP/1.1 200 OK

和第 5 行相差 1 微秒。ACK 和响应数据几乎同时发出------内核先处理了 ACK,然后 HTTP 进程把响应塞进发送缓冲区,内核立刻再发一个 PSH 包。

seq 1:231 = 230 字节,这是服务器方向的第一批数据。230 字节包含完整的 HTTP 状态行 + 所有响应头(Content-Type、Content-Length、Date 等),直到 \r\n\r\n 头部结束。


第 7 行:HTTP 响应体

yaml 复制代码
13:58:04.612408 IP 44.216.249.42.80 > 192.168.31.218.58846: Flags [P.], seq 231:486, ack 79, win 106, length 255: HTTP

和第 6 行相差 2 微秒。三个包(ACK + 响应头 + 响应体)在 3 微秒内连续发出。

seq 231:486 紧接第 6 行的 1:231,字节流连续不断。TCP 不关心 HTTP 的"消息边界",它只管按字节流传输。一个 HTTP 响应被拆成两个段是内核发送缓冲区和 MSS 的关系,TCP 完全不关心拆成几段,只要字节序号连续就行。

HTTP 没有显示状态行,因为 tcpdump 只在每个 HTTP 消息的起始段显示请求行或状态行,后续段只标记协议名。这 255 字节是响应体的 JSON 数据。

第 6 行 + 第 7 行 = 230 + 255 = 485 字节,这才是完整的 HTTP 响应。


第 8 行:确认响应头

ini 复制代码
13:58:04.612500 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 231, win 501, length 0

ack 231 = 1 + 230,确认响应头的 230 字节。

为什么没一起确认第 7 行的 255 字节(ack 486)?因为第 7 行的包还在路上。时间线:

bash 复制代码
服务器发出 seq 1:231   ──→  你收到,回 ack 231  ← 第8行
服务器发出 seq 231:486  ──→  还在路上

win 501 比第 4 行的 502 小了 1(实际窗口少了 128 字节),缓冲区被还没读走的数据占了少量空间。


第 9 行:确认响应体

ini 复制代码
13:58:04.612536 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [.], ack 486, win 501, length 0

ack 486 = 231 + 255,确认响应体的 255 字节。整个 485 字节 HTTP 响应全部确认完毕。

到这里出现了一个问题:第 8 行和第 9 行是连续两个纯 ACK,能不能合并成一个 ack 486?

理论上可以,这就是延迟 ACK 机制------Linux 默认每收到 2 个包才回一个 ACK。但这里没合并,因为第 6 行带 PSH 标志,内核看到 PSH 会立即回 ACK 不等延迟定时器。所以 PSH 不仅影响接收端交付时机,也影响 ACK 发送时机。


第 10 行:FIN------关闭连接

bash 复制代码
13:58:04.612917 IP 192.168.31.218.58846 > 44.216.249.42.80: Flags [F.], seq 79, ack 486, win 501, length 0

curl 读完响应后关闭连接。Flags [F.] = FIN + ACK。

FIN 和 SYN 一样占用一个序列号,即使 length 0。seq 79 说明自第 4 行发了 78 字节请求后再没发过数据。

这里只抓到 10 个包就停了,完整的四次挥手后续没抓到。但四次挥手不是死的------核心原则是 ACK 不能等,FIN 可以等:收到 FIN 后必须先回 ACK(对方在等确认),FIN 看时机------如果自己也没数据了,ACK+FIN 合并成一个包,就是三次挥手。几次取决于时序,不是固定模板。


全流程一图流

ini 复制代码
你 (192.168.31.218)                         服务器 (44.216.249.42)

第1行  |── SYN, seq=0 ──────────────────────>|
第2行  |<── SYN-ACK, seq=0, ack=1 ──────────|    三次握手
第3行  |── ACK, ack=1 ─────────────────────>|

第4行  |── PSH, seq=1:79, ack=1 ───────────>|    你发 HTTP 请求 (78B)
第5行  |<── ACK, ack=79 ────────────────────|    服务器确认收到

第6行  |<── PSH, seq=1:231, ack=79 ─────────|    响应头 (230B)
第7行  |<── PSH, seq=231:486, ack=79 ────────|    响应体 (255B)
第8行  |── ACK, ack=231 ───────────────────>|    确认响应头
第9行  |── ACK, ack=486 ───────────────────>|    确认响应体

第10行 |── FIN, seq=79, ack=486 ───────────>|    你关闭连接

贯穿全文的核心原则

1. ACK 不能等,数据看时机

这八个字贯穿 TCP 所有阶段------握手(SYN-ACK 把 SYN 和 ACK 合并)、传输(PSH 触发立即 ACK)、挥手(ACK 先走,FIN 看时机)。凑巧就合并,凑不巧就分开,没有固定的"几次"。

2. seq 跟自己比,ack 跟对方比

tcpdump 为两个方向各维护一个独立的基准。seq 减的是自己方向的 ISN,ack 减的是对方方向的 ISN。两个 1 来源不同,只是碰巧都显示为 1。

3. TCP 是流,不是消息

TCP 不知道"HTTP 请求""HTTP 响应"的概念,它只看到连续的字节流。485 字节的响应拆成两段还是一段,TCP 不关心,只要序号连续。应用层协议必须自己定义消息边界(HTTP 用 Content-Length 和 \r\n\r\n)。

4. length 只算 TCP 载荷

length = 应用层数据量,不含 TCP 头、IP 头、以太网帧头。一个 length 0 的纯 ACK 包在网线上实际跑了约 66 字节。


排障速查表

你看到的 意味着
只有 [S] 没有 [S.] 对方没回应(防火墙丢弃、服务没启动、网络不通)
[S] 回了 [R.] 对方明确拒绝(端口没开)
握手成功但数据传输卡住 不是连接问题,是应用层问题
ack 不涨 对方没收到新数据(丢包或延迟)
ack 跳到预期值 数据全部到达
ack 只跳了一半 部分数据到了,有丢包
大量 [R] 可能是端口扫描
MSS 不对称 一方走了隧道/封装,实际传输取较小值

写在后面

tcpdump 不可怕。可怕的是"满屏乱码"的错觉。逐字节拆开后,每一行都是固定结构:时间戳 / 源 > 目标 / 标志 / 序列号 / 确认号 / 窗口 / 选项 / 长度。没有神秘的东西,只有你没拆开的东西。

抓包 + Wireshark 是排障的标准流程:命令行抓(tcpdump -w file.pcap),GUI 看。但读懂 tcpdump 原始输出是必须会的能力------生产服务器上没有 GUI,你只有终端。

下一次面对满屏输出时,不要慌,先找 Flags,再找 seq 和 ack,数据传输的节奏自然就浮现出来了。

相关推荐
阿凡9807302 小时前
花 100 dollar,用 Claude 打通 EasyEDA&Fusion 双向同步
后端·程序员
irving同学462382 小时前
从零搭建生产级 RAG:Embedding、Chunking、Hybrid Search 与 Reranker
前端·后端
她的男孩2 小时前
从零搭一个企业后台,为什么我把能力拆成 Starter 和 Plugin
java·后端·架构
胡志辉2 小时前
本地 AI 编码助手从 0 配起来:先选模型,再接 Ollama、VS Code、Claude Code 和 Codex
前端·后端
RainCity2 小时前
Java Swing 自定义组件库分享(七)
java·笔记·后端
啷里格啷2 小时前
第二章 Fast-DDS 整体架构与分层框架
后端·架构
DolphinDB2 小时前
漫长人工,耗费存储?用 BackupRestore 模块一站式解决跨环境数据同步难题
运维·后端·架构
钟智强3 小时前
硬核自研|HunTianDB 混天DB:Rust原生工业级时序安全数据库全技术拆解
后端
_遥远的救世主_3 小时前
从一次结果集密集型查询 OOM 看 Java 服务的稳定性架构治理
java·后端