Skip to content

第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 来说:

0xc000=0xc0×256+0x00=192×256+0=49152

四字节的序列号(Sequence Number)6a 1b 2c 3e 同理:

0x6a1b2c3e=1780165694

所以,在阅读原始字节时,你要先根据字段的宽度把它切片,然后再按照网络字节序拼接起来。养成这个习惯,就能避免把 c0 00 错读成 0x00c0

固定首部的逐项定位

TCP 相对偏移长度原始字节字段样本值
0~12c0 00Source Port49152
2~3223 28Destination Port9000
4~746a 1b 2c 3eSequence Number0x6a1b2c3e
8~11410 20 30 41Acknowledgment Number0x10203041
12180Data Offset、保留/扩展控制位Data Offset = 8
13118CWR~FIN 八个 FlagsACK、PSH 置位
14~152fa f0Window原始值 64240
16~172e7 fdChecksum0xe7fd
18~19200 00Urgent Pointer0

前 20 个字节到此结束,它们构成了每一条 TCP 报文段(Segment)都必须具备的固定首部。

简单总结一下这几个字段的作用:

  • 源/目标端口:帮助接收端的主机把报文准确分发给对应的连接或监听进程。
  • 序列号(Seq):标记当前方向的数据在整个字节流里的位置。
  • 确认号(Ack):告诉对方,自己反方向已经连续收到了哪个进度的数据。
  • 标志位(Flags):传递确认(ACK)、连接建立(SYN)、连接关闭(FIN)等重要的控制信息。
  • 窗口大小(Window):向对方声明本端当前的接收能力。
  • 校验和(Checksum):用来检查数据在传输过程中有没有出现比特翻转等差错。
  • 紧急指针(Urgent Pointer):配合 URG 标志位,指示紧急数据在报文里的具体位置。

这些字段通力合作,共同描绘出 TCP 连接在当前时刻的一小段状态快照。

用两套偏移量交叉检查

“TCP 相对偏移”很适合用来查阅协议规范,而“整帧偏移”则更方便你在 Wireshark 的 Packet Bytes 区域里逐字节核对原始数据。既然我们的样本 TCP 起点在整帧的偏移量 34,它们之间的换算关系就非常直观:

Frame Offset=34+TCP Relative Offset

我们可以直接验算几个关键位置:

字段TCP 相对偏移整帧偏移首字节
Source Port034c0
Sequence Number4386a
Acknowledgment Number84210
Data Offset / Flags124680
Checksum1650e7
Options205401
Application Data326648

在实际抓包分析时,如果你发现某个字段在 Wireshark 里的高亮范围跟你手算的偏移量对不上,先别急。建议你先回头检查链路层类型、IPv4 的 IHL(首部长度)或是 IPv6 的扩展首部链,最后再检查 TCP 的 Data Offset。按这个顺序排查,通常能最快找出“坐标起点”到底偏在了哪里。

用 Data Offset 找到首部终点

让我们回到 TCP 相对偏移 12 的位置,这个字节是 0x80,把它转换成二进制看看:

text
0x80 = 1000 0000
       ^^^^
       高四位 = 8

Data Offset(数据偏移,即首部长度)占据了这个字节的高 4 位。它的数值单位是“32 位字(32-bit word)”,也就是 4 个字节。计算一下:

8×4 bytes=32 bytes

由此可知,我们的样本 TCP 首部总长是 32 个字节。除去那 20 字节的固定部分,选项区域(Options)的长度自然就是:

3220=12 bytes

4 位二进制数最多能表示 015。一个有效的 TCP 首部至少要包含 5 个字(即 20 字节的固定首部)。那么它的最大长度呢?自然就是 Data Offset 取最大值 15 时的情况:

15×4=60 bytes

所以,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 报文段的总长度:

6220=42 bytes

接着,再减去由 Data Offset 算出来的 32 字节 TCP 首部:

4232=10 bytes

剩下的这 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 1Wireshark 为了方便人类阅读而做的相对化处理
分析器推断值报文被标记为重传(Retransmission)Wireshark 的专家系统结合上下文做出的高级判断

建议你在抓包分析时,养成记录字段具体路径和原始字节的好习惯,这样无论分析推断多复杂,你随时都能退回最底层去复核。至于刚刚提到的窗口缩放(Window Scale)、相对序列号(Relative Seq)以及重传标签等进阶概念,我们会在后面的章节里详细展开。

一分钟首部检查清单

面对一条全新的 TCP 数据报文,你可以按照下面这套“组合拳”快速摸清它的底细:

  1. 从 IP 层确认其上层协议为 TCP,并准确定位出 TCP 首部的起点。
  2. 读取前 4 个字节,翻译出源端口、目标端口,并明确这条报文的方向。
  3. 按照 4 字节边界,分别读取 Seq 和 Ack,并在脑海里标注出它们各自代表的字节流方向。
  4. 提取 Data Offset 算出实际的首部长度,以此为界,划出 Options 选项区和 Payload 载荷区的分界线。
  5. 用 IP 层提供的总长度,减去 IP 首部和 TCP 首部的长度,算出实际的数据载荷大小(即 tcp.len)。
  6. 读取 Flags 标志位,判断这条报文主要是在传输数据,还是在执行连接控制任务(比如握手或挥手)。
  7. 顺手记录下 Window 的原始字段值、Checksum 的显示状态以及有哪些选项列表,为后续结合上下文深入分析留下“证据”。

拿我们的样本报文来走一遍这套流程,你就能瞬间提炼出这样一句摘要:“客户端 49152 发往服务端 9000,TCP 首部长 32 字节,携带 10 字节 Payload 数据,同时 ACK 和 PSH 标志位置位。”有了这句话,报文的方向、空间布局以及它承担的职责,就全都被你拿捏住了,为接下来的深度分析打下了坚实基础。

在 Wireshark 中完成一次手工解析

实验步骤

  1. 使用显示过滤器 tcp.port == 9000 && tcp.len > 0,挑选出一条带数据的 TCP 报文。
  2. 在 Packet Details(数据包详情)面板里,先把所有的协议树都折叠起来,只单独展开 Transmission Control Protocol 这一层。
  3. 把目光移向 Packet Bytes(数据包字节)面板,找到 TCP 的首字节起点。如果是 IPv4,你可以用“链路层首部长度 + IHL”来算;如果是 IPv6,那就沿着 Next Header 链逐级找下去。
  4. 找到起点后,直接把前 20 个字节抄录下来,并且按照 2、2、4、4、2、2、2、2 的字节长度进行手工切分。
  5. 提取这 20 字节中的 Data Offset 字段,自己动笔算一算完整的 TCP 首部到底有多长。
  6. 结合 IP 层提供的总长度,减去各类首部,算出 TCP Payload 的实际大小。
  7. 最后,把你手工推算的这一套结果,跟 Wireshark 自动解析出来的 tcp.srcporttcp.dstporttcp.seq_rawtcp.ack_rawtcp.hdr_len 还有 tcp.len 逐一核对,看看是否严丝合缝。

预期现象

  • 源端口和目标端口,可以直接从开头的 4 个字节里完美翻译出来。
  • 树状节点里的 tcp.seq_rawtcp.ack_raw 应该完全对应你抄下来的那两串原始 32 位字节;不过,Wireshark 主界面的摘要信息区大概率会显示那个友好的“相对序列号”。
  • 只要 TCP 首部长度算出来大于 20 字节,你在协议树里就一定能看到固定首部之后冒出来一个 Options 选项节点。
  • tcp.len 的数值,必定等于 IP 层的载荷总长度减去你算出来的 TCP 首部长度。
  • 最神奇的是,当你在协议树里随便点击一个字段时,下方的 Packet Bytes 面板就会精准地高亮显示出它所占用的那些位或字节,分毫不差。

理解检查

  1. 假设 TCP 起点后的第 12 字节是 0x70。请问此时的 Data Offset 是多少?整个 TCP 首部有多长?其中包含的选项区域又是多长?
  2. 如果抓到一条包,IPv4 Total Length 显示 84 字节,IHL 是 5,TCP Data Offset 是 7。请问 TCP Payload(载荷)到底有多少字节?
  3. 当报文方向反转,由服务端 9000 发回给客户端 49152 时,TCP 首部最前面的 4 个字节应该长什么样?
  4. 如果 Wireshark 界面显示 Seq = 1,而底层原始 4 字节数据是 6a 1b 2c 3e。如果你要对着抓包文件逐字节手工核对,你应该用哪一个值?
参考答案
  1. 0x70 的高 4 位是 7,所以 Data Offset 是 7。由此得出首部总长度为 7×4=28 字节,减去固定首部,选项区域为 2820=8 字节。
  2. IPv4 首部长度为 5×4=20 字节,TCP 首部长度为 7×4=28 字节。由此可推导出 Payload 为 842028=36 字节。
  3. 此时源端口是 9000(十六进制 0x2328),目标端口是 49152(十六进制 0xc000)。排在一起,前 4 字节就是 23 28 c0 00
  4. 原始值 0x6a1b2c3e 才是实打实传输的线上字节,只有它能跟帧里的 16 进制数据直接核对;而相对值 1,只是 Wireshark 为了让你一眼看清连接进度而贴心算出来的显示值而已。

本章小结

  • TCP 的固定首部雷打不动占 20 字节,而变长的选项决定了 Data Offset 的值,进而决定了整个首部的总长度。
  • 我们的实战样本里,Data Offset 等于 8,意味着首部总长 32 字节,其中选项部分塞了 12 字节的数据。
  • 处理多字节整数时要时刻记得网络字节序(大端)。手撕报文的正确姿势是:先根据字段宽度把字节切出来,然后再从高位到低位拼合。
  • TCP 的数据载荷(Payload)大小并不直接写在 TCP 首部里,而是需要用 IP 层的总长度,一层一层剥去 IP 首部和 TCP 首部才能算出来。
  • 一条 TCP 报文兼顾两个方向:Seq 代表自己出去的发货进度,而 Ack 和 Window 则是给对端反馈的反向收货情况。
  • Wireshark 界面上的信息,可以拆解为原始字节、协议解析、上下文推导、阅读优化和专家系统分析这五个不同的“证据层次”,懂得区分它们,是避免被高级功能误导的关键。

上一章:第7章 网络包的分层结构 · 所属篇:第三篇 · 下一章:第9章 源端口和目标端口 · 教程总览

Licensed under CC BY-NC-SA 4.0.