Appearance
第8章 TCP 首部总览
在上一章,我们算出了样本 TCP 报文段在整个数据帧中从偏移量 34 开始。现在,我们把坐标原点移到 TCP 协议的首字节——也就是说,将 TCP 相对偏移量 0 对齐到整帧的偏移量 34。有了这个“局部坐标系”,我们就能把这段 42 字节的 TCP 报文段,精确拆解为 32 字节的首部(Header)和 10 字节的数据载荷(Payload)。
本章我们先从宏观上鸟瞰 TCP 首部的整体布局。至于各个字段的具体含义,会在接下来的第 9 到 14 章逐一深入探讨。
先读原始字节,再看解析结果
我们的样本 TCP 报文段如下,左侧的偏移量已经以 TCP 首字节为零点重新对齐:
text
TCP+00 c0 00 23 28 6a 1b 2c 3e 10 20 30 41 80 18 fa f0
TCP+10 e7 fd 00 00 01 01 08 0a 01 02 03 04 a0 b0 c0 d0
TCP+20 48 45 4c 4c 4f 20 54 43 50 0a这里的 TCP+10 表示十六进制的偏移量 0x10(即十进制的 16)。同理,TCP+20 对应十进制的 32,刚好是这段应用数据(Application Data)的起点。
TCP 首部的固定部分占据 20 个字节,后面还可以跟着长度可变的选项(Options)。你可以先在脑海中印下这张经典的 TCP 首部布局图:
text
TCP 相对偏移 0 1 2 3
+-------------------+-------------------+-------------------+-------------------+
0~3 | Source Port 16 bit | Destination Port 16 bit |
+---------------------------------------+---------------------------------------+
4~7 | Sequence Number 32 bit |
+-------------------------------------------------------------------------------+
8~11 | Acknowledgment Number 32 bit |
+---------+-------------+---+-----------------------------------+----------------+
12~15 |DOffset 4| Reserved 3 |AE | CWR ECE URG ACK PSH RST SYN FIN | Window 16 bit |
+---------+-------------+---+-----------------------------------+----------------+
16~19 | Checksum 16 bit | Urgent Pointer 16 bit |
+---------------------------------------+---------------------------------------+
20~首部末尾 | Options 与填充,长度由 Data Offset 决定 |
+-------------------------------------------------------------------------------+
首部末尾之后 | Application Data ... |
+-------------------------------------------------------------------------------+图中的 AE 是 RFC 9768 最新分配的扩展标志位;如果系统尚未启用该扩展,遇到不认识的标志位时,直接按原来的“保留位(Reserved)”规则处理即可。接下来,我们会在下表中详细展开每个字段对应的原始字节范围。
网络字节序:先确定怎样拼接多个字节
TCP 首部里的 16 位和 32 位整数都采用网络字节序(Network Byte Order),也就是所谓的大端模式(Big-Endian),高位字节排在前面。就拿源端口(Source Port)的这两个字节 c0 00 来说:
四字节的序列号(Sequence Number)6a 1b 2c 3e 同理:
所以,在阅读原始字节时,你要先根据字段的宽度把它切片,然后再按照网络字节序拼接起来。养成这个习惯,就能避免把 c0 00 错读成 0x00c0。
固定首部的逐项定位
| TCP 相对偏移 | 长度 | 原始字节 | 字段 | 样本值 |
|---|---|---|---|---|
| 0~1 | 2 | c0 00 | Source Port | 49152 |
| 2~3 | 2 | 23 28 | Destination Port | 9000 |
| 4~7 | 4 | 6a 1b 2c 3e | Sequence Number | 0x6a1b2c3e |
| 8~11 | 4 | 10 20 30 41 | Acknowledgment Number | 0x10203041 |
| 12 | 1 | 80 | Data Offset、保留/扩展控制位 | Data Offset = 8 |
| 13 | 1 | 18 | CWR~FIN 八个 Flags | ACK、PSH 置位 |
| 14~15 | 2 | fa f0 | Window | 原始值 64240 |
| 16~17 | 2 | e7 fd | Checksum | 0xe7fd |
| 18~19 | 2 | 00 00 | Urgent Pointer | 0 |
前 20 个字节到此结束,它们构成了每一条 TCP 报文段(Segment)都必须具备的固定首部。
简单总结一下这几个字段的作用:
- 源/目标端口:帮助接收端的主机把报文准确分发给对应的连接或监听进程。
- 序列号(Seq):标记当前方向的数据在整个字节流里的位置。
- 确认号(Ack):告诉对方,自己反方向已经连续收到了哪个进度的数据。
- 标志位(Flags):传递确认(ACK)、连接建立(SYN)、连接关闭(FIN)等重要的控制信息。
- 窗口大小(Window):向对方声明本端当前的接收能力。
- 校验和(Checksum):用来检查数据在传输过程中有没有出现比特翻转等差错。
- 紧急指针(Urgent Pointer):配合 URG 标志位,指示紧急数据在报文里的具体位置。
这些字段通力合作,共同描绘出 TCP 连接在当前时刻的一小段状态快照。
用两套偏移量交叉检查
“TCP 相对偏移”很适合用来查阅协议规范,而“整帧偏移”则更方便你在 Wireshark 的 Packet Bytes 区域里逐字节核对原始数据。既然我们的样本 TCP 起点在整帧的偏移量 34,它们之间的换算关系就非常直观:
我们可以直接验算几个关键位置:
| 字段 | TCP 相对偏移 | 整帧偏移 | 首字节 |
|---|---|---|---|
| Source Port | 0 | 34 | c0 |
| Sequence Number | 4 | 38 | 6a |
| Acknowledgment Number | 8 | 42 | 10 |
| Data Offset / Flags | 12 | 46 | 80 |
| Checksum | 16 | 50 | e7 |
| Options | 20 | 54 | 01 |
| Application Data | 32 | 66 | 48 |
在实际抓包分析时,如果你发现某个字段在 Wireshark 里的高亮范围跟你手算的偏移量对不上,先别急。建议你先回头检查链路层类型、IPv4 的 IHL(首部长度)或是 IPv6 的扩展首部链,最后再检查 TCP 的 Data Offset。按这个顺序排查,通常能最快找出“坐标起点”到底偏在了哪里。
用 Data Offset 找到首部终点
让我们回到 TCP 相对偏移 12 的位置,这个字节是 0x80,把它转换成二进制看看:
text
0x80 = 1000 0000
^^^^
高四位 = 8Data Offset(数据偏移,即首部长度)占据了这个字节的高 4 位。它的数值单位是“32 位字(32-bit word)”,也就是 4 个字节。计算一下:
由此可知,我们的样本 TCP 首部总长是 32 个字节。除去那 20 字节的固定部分,选项区域(Options)的长度自然就是:
4 位二进制数最多能表示 0 到 15。一个有效的 TCP 首部至少要包含 5 个字(即 20 字节的固定首部)。那么它的最大长度呢?自然就是 Data Offset 取最大值 15 时的情况:
所以,TCP 首部的长度范围必定在 20 到 60 字节之间,这就意味着选项区域最多只能塞下 40 个字节。我们这个样本里的 12 字节选项长这样:
text
01 01 08 0a 01 02 03 04 a0 b0 c0 d0仔细看,它包含两个 NOP(无操作)填充字节和一个 Timestamp(时间戳)选项。到了第 14 章,我们会逐字节教你怎么解析它里面的 Kind、Length 和 Value。
从两个长度字段算出 Payload
在第 7 章里,我们已经读出 IPv4 层的 Total Length 是 62 字节,而 IHL 告诉我们 IPv4 首部占了 20 字节。那我们先算出整个 TCP 报文段的总长度:
接着,再减去由 Data Offset 算出来的 32 字节 TCP 首部:
剩下的这 10 个字节,就是 Wireshark 里的 tcp.len。它正是真正参与 TCP 序列号推进的数据载荷(Payload)部分:
text
48 45 4c 4c 4f 20 54 43 50 0a -> HELLO TCP\n留意一下,这里出现了两个层面的“长度”概念:一个是 IP 层的 Total Length,它是明确写在 IPv4 首部字段里的;而 TCP 的报文段长度和 Payload 长度,则是靠 IP 层划定的总边界和 TCP 自己的 Data Offset 共同推导出来的。
Data Offset 与 Flags 共享相邻位区
最早的 TCP 首部布局图是按 32 位(4 字节)宽度来画的。在这个布局里,Data Offset 霸占了第 12 个字节的高 4 位,紧接着的 3 位是保留位,而这个字节的最低位给了 AE 标志。接下来到了第 13 个字节,从高位到低位依次排开了 CWR、ECE、URG、ACK、PSH、RST、SYN 和 FIN。在 Wireshark 里,这 9 个控制标志位会被贴心地展开成各自独立的字段。
在我们的样本中,主要包含标志位的那个字节是 0x18:
text
0x18 = 0001 1000
│└─ FIN = 0
└── SYN = 0
1──── PSH = 1
1───── ACK = 1从低位向高位依次对应 FIN、SYN、RST、PSH、ACK、URG、ECE 到 CWR,你会清楚地看到,ACK 和 PSH 被置为了 1。至于这些标志位组合在一起到底代表了什么上下文语义,我们会在第 12 章借助完整的时间线来为你解密。
首部中的两个方向
有趣的是,别看它只是一条单向发出的 TCP 报文段,实际上它同时携带着两个方向上的信息:
- 源端口(Source Port)和序列号(Sequence Number):描述的是当前报文发送方向上的数据进度。
- 目标端口(Destination Port):指向对端接收服务的监听端口,或是已经建立好的连接端点。
- 确认号(Acknowledgment Number)和窗口(Window):则是发送方向对方“通报”自己这边反方向字节流的接收进度和接收能力。
因为我们的样本是从客户端发往服务端的,所以 Seq = 0x6a1b2c3e 代表的是“客户端到服务端”这个方向的数据流水线进度;而 Ack = 0x10203041,指的则是“服务端到客户端”方向的接收进度。TCP 总是尽职地为这两个方向各自维护一条序列号时间线。当你观察服务端的响应报文时,源端口和目标端口会对调,Seq 和 Ack 代表的方向自然也会随之交换。
原始值、解释值与显示值
在 Wireshark 的协议解析树里,你往往会看到同一个字段同时展示出好几个不同“层次”的值:
| 类型 | 样本例子 | 来源 |
|---|---|---|
| 原始值 | Seq 字节 6a 1b 2c 3e | 抓包文件里实打实的二进制字节 |
| 协议解释值 | Data Offset 8 代表 32 字节 | TCP 协议规范对原始字段的解码 |
| 上下文解释值 | Window 经 Window Scale 计算后的真实窗口大小 | 结合之前握手阶段的协商结果算出来的实际值 |
| 阅读优化值 | 相对 Seq 1 | Wireshark 为了方便人类阅读而做的相对化处理 |
| 分析器推断值 | 报文被标记为重传(Retransmission) | Wireshark 的专家系统结合上下文做出的高级判断 |
建议你在抓包分析时,养成记录字段具体路径和原始字节的好习惯,这样无论分析推断多复杂,你随时都能退回最底层去复核。至于刚刚提到的窗口缩放(Window Scale)、相对序列号(Relative Seq)以及重传标签等进阶概念,我们会在后面的章节里详细展开。
一分钟首部检查清单
面对一条全新的 TCP 数据报文,你可以按照下面这套“组合拳”快速摸清它的底细:
- 从 IP 层确认其上层协议为 TCP,并准确定位出 TCP 首部的起点。
- 读取前 4 个字节,翻译出源端口、目标端口,并明确这条报文的方向。
- 按照 4 字节边界,分别读取 Seq 和 Ack,并在脑海里标注出它们各自代表的字节流方向。
- 提取 Data Offset 算出实际的首部长度,以此为界,划出 Options 选项区和 Payload 载荷区的分界线。
- 用 IP 层提供的总长度,减去 IP 首部和 TCP 首部的长度,算出实际的数据载荷大小(即
tcp.len)。 - 读取 Flags 标志位,判断这条报文主要是在传输数据,还是在执行连接控制任务(比如握手或挥手)。
- 顺手记录下 Window 的原始字段值、Checksum 的显示状态以及有哪些选项列表,为后续结合上下文深入分析留下“证据”。
拿我们的样本报文来走一遍这套流程,你就能瞬间提炼出这样一句摘要:“客户端 49152 发往服务端 9000,TCP 首部长 32 字节,携带 10 字节 Payload 数据,同时 ACK 和 PSH 标志位置位。”有了这句话,报文的方向、空间布局以及它承担的职责,就全都被你拿捏住了,为接下来的深度分析打下了坚实基础。
在 Wireshark 中完成一次手工解析
实验步骤
- 使用显示过滤器
tcp.port == 9000 && tcp.len > 0,挑选出一条带数据的 TCP 报文。 - 在 Packet Details(数据包详情)面板里,先把所有的协议树都折叠起来,只单独展开 Transmission Control Protocol 这一层。
- 把目光移向 Packet Bytes(数据包字节)面板,找到 TCP 的首字节起点。如果是 IPv4,你可以用“链路层首部长度 + IHL”来算;如果是 IPv6,那就沿着 Next Header 链逐级找下去。
- 找到起点后,直接把前 20 个字节抄录下来,并且按照 2、2、4、4、2、2、2、2 的字节长度进行手工切分。
- 提取这 20 字节中的 Data Offset 字段,自己动笔算一算完整的 TCP 首部到底有多长。
- 结合 IP 层提供的总长度,减去各类首部,算出 TCP Payload 的实际大小。
- 最后,把你手工推算的这一套结果,跟 Wireshark 自动解析出来的
tcp.srcport、tcp.dstport、tcp.seq_raw、tcp.ack_raw、tcp.hdr_len还有tcp.len逐一核对,看看是否严丝合缝。
预期现象
- 源端口和目标端口,可以直接从开头的 4 个字节里完美翻译出来。
- 树状节点里的
tcp.seq_raw和tcp.ack_raw应该完全对应你抄下来的那两串原始 32 位字节;不过,Wireshark 主界面的摘要信息区大概率会显示那个友好的“相对序列号”。 - 只要 TCP 首部长度算出来大于 20 字节,你在协议树里就一定能看到固定首部之后冒出来一个 Options 选项节点。
tcp.len的数值,必定等于 IP 层的载荷总长度减去你算出来的 TCP 首部长度。- 最神奇的是,当你在协议树里随便点击一个字段时,下方的 Packet Bytes 面板就会精准地高亮显示出它所占用的那些位或字节,分毫不差。
理解检查
- 假设 TCP 起点后的第 12 字节是
0x70。请问此时的 Data Offset 是多少?整个 TCP 首部有多长?其中包含的选项区域又是多长? - 如果抓到一条包,IPv4 Total Length 显示 84 字节,IHL 是 5,TCP Data Offset 是 7。请问 TCP Payload(载荷)到底有多少字节?
- 当报文方向反转,由服务端
9000发回给客户端49152时,TCP 首部最前面的 4 个字节应该长什么样? - 如果 Wireshark 界面显示
Seq = 1,而底层原始 4 字节数据是6a 1b 2c 3e。如果你要对着抓包文件逐字节手工核对,你应该用哪一个值?
参考答案
0x70的高 4 位是 7,所以 Data Offset 是 7。由此得出首部总长度为 字节,减去固定首部,选项区域为 字节。- IPv4 首部长度为
字节,TCP 首部长度为 字节。由此可推导出 Payload 为 字节。 - 此时源端口是
9000(十六进制0x2328),目标端口是49152(十六进制0xc000)。排在一起,前 4 字节就是23 28 c0 00。 - 原始值
0x6a1b2c3e才是实打实传输的线上字节,只有它能跟帧里的 16 进制数据直接核对;而相对值1,只是 Wireshark 为了让你一眼看清连接进度而贴心算出来的显示值而已。
本章小结
- TCP 的固定首部雷打不动占 20 字节,而变长的选项决定了 Data Offset 的值,进而决定了整个首部的总长度。
- 我们的实战样本里,Data Offset 等于 8,意味着首部总长 32 字节,其中选项部分塞了 12 字节的数据。
- 处理多字节整数时要时刻记得网络字节序(大端)。手撕报文的正确姿势是:先根据字段宽度把字节切出来,然后再从高位到低位拼合。
- TCP 的数据载荷(Payload)大小并不直接写在 TCP 首部里,而是需要用 IP 层的总长度,一层一层剥去 IP 首部和 TCP 首部才能算出来。
- 一条 TCP 报文兼顾两个方向:Seq 代表自己出去的发货进度,而 Ack 和 Window 则是给对端反馈的反向收货情况。
- Wireshark 界面上的信息,可以拆解为原始字节、协议解析、上下文推导、阅读优化和专家系统分析这五个不同的“证据层次”,懂得区分它们,是避免被高级功能误导的关键。