Appearance
第10章 Sequence Number:给字节编号
TCP 向应用层提供有序的字节流。但在底层,网络可能会将一段数据拆分成多个报文段(Segment),这些报文段到达的时间和顺序往往也是错乱的。Sequence Number(序列号,简称 Seq)的作用就是为字节流中的每个位置提供统一的坐标,让接收方能够正确地重排数据、识别数据缺口,并把重传的数据放到正确的位置上。
本章我们将继续使用之前的 10 字节数据样本:
text
Seq = 0x6a1b2c3e
Payload = 48 45 4c 4c 4f 20 54 43 50 0a
H E L L O T C P \nSeq 指向当前报文段的首个数据字节
TCP 首部中的 Sequence Number 字段占用 32 位(bit)。当报文段携带数据时,Seq 表示该报文段中第一个数据字节,在当前发送方向的整个字节流空间里所处的位置。
在我们提供的样本中,第一个字节 H 的编号(即 Seq)是 0x6a1b2c3e。随后的每个字节,其编号依次加 1:
| Payload 偏移 | 字节 | 字符 | 线上序列号 |
|---|---|---|---|
| 0 | 48 | H | 0x6a1b2c3e |
| 1 | 45 | E | 0x6a1b2c3f |
| 2 | 4c | L | 0x6a1b2c40 |
| 3 | 4c | L | 0x6a1b2c41 |
| 4 | 4f | O | 0x6a1b2c42 |
| 5 | 20 | 空格 | 0x6a1b2c43 |
| 6 | 54 | T | 0x6a1b2c44 |
| 7 | 43 | C | 0x6a1b2c45 |
| 8 | 50 | P | 0x6a1b2c46 |
| 9 | 0a | 换行 | 0x6a1b2c47 |
这组 10 字节的数据占据了一个左闭右开的序列号区间:
区间的左端点属于当前报文段,而右端点则是下一段新数据的起点。数据长度对应的计算过程如下:
因此,该发送方向上的下一个 Seq 就是 0x6a1b2c48。在第11章中我们还会看到,当接收方完整收到这 10 个字节后,它回复的 Acknowledgment Number(Ack)也会刚好指向这个右端点。
Seq 编号的对象是字节位置
假设应用程序一次性向 Socket 写入了 10 字节数据 HELLO TCP\n。TCP 在底层可以将它们打包成一个单独的报文段:
text
段 A:Seq = 1,Len = 10,覆盖 [1, 11)当然,TCP 也可以将它们拆分成两个报文段发送:
text
段 A:Seq = 1,Len = 4,覆盖 [1, 5) -> HELL
段 B:Seq = 5,Len = 6,覆盖 [5, 11) -> O TCP\n无论是哪种分段方式,它们覆盖的都是同一个字节区间 [1, 11),接收方的应用程序最终读到的也是完全相同的有序字节流。这个例子说明了一个核心概念:Seq 的连续性是建立在“字节位置”之上的。至于报文段的边界(即如何切分数据),则会随着 MSS(最大报文段长度)、拥塞状态(Congestion Control)、发送时机以及网卡卸载(Offload)等因素动态变化。
在拆分发送的情况下,如果第二个报文段先到达,接收方可以根据 Seq = 5 把它安置在后半段的位置,并继续等待接收区间 [1, 5) 的数据。如果第一个报文段丢失需要重传,它的重传副本依然会使用 Seq = 1 以及相同的覆盖区间。正是因为有了序列号,TCP 才能在面对乱序到达和重复报文时,拥有统一的识别和处理依据。
双向通信拥有独立的序列号空间
TCP 提供的是全双工通信(Full-Duplex)。在建立连接时,客户端到服务端,以及服务端到客户端这两个方向,会各自独立生成一个初始序列号,并在后续的数据传输中分别向前推进:
text
客户端 -> 服务端:客户端 Seq 空间
服务端 -> 客户端:服务端 Seq 空间回到我们的样本字段:
text
Seq = 0x6a1b2c3e
Ack = 0x10203041在这个例子中,Seq 描述的是客户端当前的发送进度,而 Ack 则指向服务端发送方向的进度(即客户端期望从服务端收到的下一个字节位置)。当客户端发送 10 字节后,客户端方向的下一个 Seq 会推进到 0x6a1b2c48;至于服务端方向的 Seq 会如何推进,则完全取决于服务端实际发送了多少数据,以及是否发送了 SYN 或 FIN 控制位。
在响应报文中,双方的角色会发生对换。例如,在服务端还没有准备好发送应用层数据时,它回复的纯 ACK(不带数据的确认报文)可能会是这样的:
text
Server Seq = 0x10203041
Server Ack = 0x6a1b2c48可以看到,这里的两个数值恰好分别对应两套独立的序列号空间。关于 Ack 累计确认(Cumulative Acknowledgment)的具体语义,我们将在第11章中为您详细讲解。
SYN 和 FIN 控制位各占用一个序列号
除了实际的数据字节会占用序列号空间外,SYN 和 FIN 这两个控制位在被设置时,也会各自“消耗” 1 个序列号位置。因此,在发送完一个报文段后,下一个报文段的起始序列号(NextSeq)可以通过以下公式统一计算:
其中:
是 TCP Payload 的真实字节数;- 当 SYN 置位时
,其余情况 ; - 当 FIN 置位时
,其余情况 。
需要注意的是,像 ACK、PSH、RST 这样的标志位,都不会消耗序列号空间。同时,TCP Options(TCP 选项)属于首部的一部分,因此选项的长度也不会计入数据长度
情形一:纯 ACK
text
Seq = 7000,Payload Len = 0,SYN = 0,FIN = 0
NextSeq = 7000 + 0 + 0 + 0 = 7000由于计算结果没有增加,纯 ACK 可以连续多次发送且使用相同的 Seq。它纯粹是为了向对方报告接收状态,而本方向的新数据起点仍然保持在原位置。
情形二:SYN 携带选项
假设客户端的初始序列号 ISN 为 0x6a1b2c3d,其发出的 SYN 报文携带了 20 字节的 TCP Options,但 Payload 长度为 0:
这也正是我们在前面样本中首段数据所使用的 Seq。在这个过程中,20 字节的 TCP Options 只是扩充了 TCP 首部的长度,并未占用序列号空间;只有 SYN 标志本身消耗了 1 个序列号。
情形三:普通数据
在这个样本中,TCP 标志位(Flags)包含了 ACK 和 PSH,且承载了 10 字节的数据(Payload):
情形四:FIN 与数据同行
假设某个报文段的 Seq 从 5000 开始,它携带了 3 字节的数据,并且同时设置了 FIN 标志位:
text
数据字节位置:5000、5001、5002
FIN 的位置: 5003
NextSeq: 5004套用前面的公式,也能得出同样的结果:
当对端接收并确认这段数据及 FIN 标志时,它回复的累计 Ack 同样会指向 5004。
ISN:初始序列号从何而来
在 TCP 三次握手期间,通信双方会各自挑选一个 32 位的初始序列号(Initial Sequence Number,简称 ISN)。客户端会在其发送的 SYN 报文中声明自己的 ISN,而服务端则会在 SYN+ACK 报文中声明服务端的 ISN。握手完成后,双方发送的首个实际数据字节的编号,都会从各自的 ISN 加 1 开始。
我们在本教程样本中做如下约定:
text
客户端 ISN = 0x6a1b2c3d
服务端 ISN = 0x10203040于是握手之后:
text
客户端首个数据 Seq = 0x6a1b2c3e
服务端首个数据 Seq = 0x10203041现代操作系统的 TCP 协议栈会根据相关规范,结合安全策略,动态生成具备一定随机性和安全性的 ISN,以防止序列号预测攻击(Sequence Number Prediction Attack)。在进行抓包分析时,我们不需要关心 ISN 的具体生成算法,只需从握手报文中读取双方的原始 Seq 作为基准,随后顺着两个方向分别进行推算即可。
Wireshark 中的相对序列号(Relative Sequence Number)
在排查网络问题时,像 1780165694 这样庞大的十进制原始序列号读起来非常费力。为了方便分析,Wireshark 默认开启了“相对序列号”(Relative sequence numbers)功能。它会将每个方向上捕获到的第一个起始序列号作为基准点映射为 0,之后的序列号都显示为相对于这个基准的偏移量。
以客户端方向为例:
| 报文 | 线上原始 Seq | Wireshark 相对 Seq | 序列号消耗 |
|---|---|---|---|
| SYN | 0x6a1b2c3d | 0 | 1 |
| 首段 10 字节数据 | 0x6a1b2c3e | 1 | 10 |
| 下一段新数据 | 0x6a1b2c48 | 11 | 取决于新 Payload |
相对序列号的计算逻辑可以简单理解为:
对样本的首段数据计算如下:
在 Wireshark 中,字段 tcp.seq_raw 保存的是抓包到的真实原始值,而 tcp.seq 则是根据当前显示设置计算出的结果(通常就是相对值)。如果你在撰写排查分析报告,建议标注清楚所使用的到底是原始值还是相对值。通常来说,相对值更适合用来展示数据流的推进关系;而原始值则在核对底层字节流(Packet Bytes)或对比多款抓包工具的结果时更加可靠。
需要特别注意的一点是,如果你是从 TCP 连接中途开始抓包的(即没有抓到握手阶段),Wireshark 只能把你抓到的第一个报文当作显示基准。此时,这个基准值与真实握手时的 ISN 之间究竟相差多少,往往需要通过其他报文证据才能确认。
32 位序列号的回绕与模运算
因为 Sequence Number 字段只有 32 位,所以它的数值范围是固定且有限的:
当序列号不断累加,超过这个最大值后,它会发生“回绕”(Wrap Around),即从 0 开始重新计数。让我们来看一个即将回绕的例子:
text
Seq = 0xfffffffc = 4294967292
Payload Len = 8如果用普通的整数加法计算,结果为:
但在 32 位的空间里,我们需要对
所以正确的下一跳序列号是:
text
NextSeq = 0x00000004这 8 个字节的序列号编号依次是:
text
fffffffc fffffffd fffffffe ffffffff 00000000 00000001 00000002 00000003在 TCP 的协议规范中,正是通过这种基于序列号空间的模运算,配合接收窗口的大小,来判断报文的新旧与先后关系的。现代化的抓包分析工具(如 Wireshark)会自动帮你处理这种回绕现象;但在手工计算和分析跨越最大值边界的报文时,一定要记住顺着这个环形(Ring)空间去推导。
网卡卸载(Offload)会改变本机抓包的观察单位
现代操作系统和网卡通常会启用 TSO(TCP Segmentation Offload)或 GSO(Generic Segmentation Offload)等硬件加速功能。在开启这些功能的情况下,如果在发送端主机上进行抓包,你有可能会看到一个体积远超 MTU 的“巨型” TCP 数据段。这是因为操作系统的网络栈将分段工作交给了网卡硬件,而抓包点往往位于网卡分段操作之前,因此抓到的是未拆分的数据。网卡在随后发送到线路上时,才会真正将其切分成一个个适合网络传输的小报文段。同理,接收端开启的合并卸载(如 GRO/LRO)也会将多个小报文合并,导致接收侧主机抓包时看到“偏大”的报文单元。
不过无需担心,序列号的计算规则依然保持一致:这个“巨型”数据段覆盖的区间跨度,仍然严格等于它的起始 Seq 加上其承载的 TCP 数据长度。关于如何通过不同的抓包位置和卸载状态来辨认这类现象,我们将留在第32章中详细探讨。
动手实验:在相对值与原始值之间切换
实验步骤
- 捕获一条从 SYN 开始的完整连接,显示过滤器使用
tcp.stream == N。 - 为数据包列表添加
tcp.seq、tcp.seq_raw、tcp.len三列,或在字段树中逐项查看。 - 在 Wireshark 的 TCP 协议首选项中记录 Relative sequence numbers 的当前状态。
- 依次记录客户端 SYN、第一条客户端数据、下一条客户端数据的 Seq 与 Len。
- 用
计算每条报文的右端点。 - 切换相对序列号显示,再次读取同一批报文;检查 Packet Bytes 中的四个原始字节是否保持原值。
- 若出现重传,比较重传报文与原报文的原始 Seq、Len 与覆盖区间。
预期现象
- 在抓取到完整握手过程的前提下,SYN 报文的相对 Seq 通常会显示为 0,而随后发送的首个数据字节的相对 Seq 将从 1 开始。
- 对于普通的数据报文段,其下一个 Seq 将严格等于当前的 Seq 加上
tcp.len。 - 纯 ACK 报文的
tcp.len为 0,因此本发送方向的 Seq 会维持不变,停留在新数据的起点。 - 发生重传时,重传的报文段会再次覆盖先前已经分配的序列号区间。
- 切换 Wireshark 的“相对序列号”显示设置,仅仅会改变界面上的数值展示方式,底层的 pcap 抓包文件中实际记录的那 4 个字节的 Seq 原始值不会发生任何改变。
小测验:理解检查
- 假设有一条纯 ACK 报文,它的 Seq 为 700,且 Payload Len 为 0。请问该方向上的下一个 Seq 是多少?
- 某条数据报文段的 Seq 为 1001,Payload Len 为 400。请问它覆盖了哪个左闭右开区间?下一个 Seq 是多少?
- 有一条报文的 Seq 为 3000,其中携带了 2 个字节的数据,并且设置了 FIN 控制位。请问下一个 Seq 是多少?
- 有一条 SYN 报文,它的 Seq 为
0xabcdef00,不仅包含 40 字节的 TCP Options 选项,Payload 长度为 0。请问下一个 Seq 应该推进到哪里? - 某报文的 Seq 已经达到了
0xfffffffe,且 Payload Len 为 4。请按照 32 位空间的模运算规则,计算出下一个 Seq。
参考答案
。由于没有数据也没有 SYN/FIN,序列号不会推进。- 它覆盖的区间为
,下一个 Seq 也就是右端点,即 1401。 。2 字节数据消耗 2 个位置,FIN 控制位额外消耗 1 个位置。- TCP Options 选项位于首部,对序列号空间的贡献为 0,只有 SYN 控制位消耗 1 个位置,所以下一个 Seq 为
0xabcdef01。 0xfffffffe + 4的结果对 取模,发生回绕,最终结果为0x00000002。
本章小结
- Seq(Sequence Number)指向当前发送方向中,当前报文段所承载的第一个数据字节在整个字节流中的位置。
- 长度为
的数据报文段,会覆盖序列号空间中左闭右开的区间 。 - SYN 和 FIN 这两个控制位在被设置时,也会各占用一个序列号位置;而 TCP Options 选项仅扩充 TCP 首部,不消耗序列号空间。
- TCP 连接中的两个发送方向拥有互相独立、各自生成的初始序列号(ISN)和序列号空间。
- 发生数据重传时,重传报文会复用原来的序列号区间;而乱序到达的报文段也是依靠 Seq 来找回自己在字节流中的正确位置。
- Wireshark 提供的“相对序列号”功能更加便于我们阅读和分析推进关系,但如需查看真实的四字节原始值,应参考
tcp.seq_raw字段。 - 32 位的序列号空间是环形的,它按
进行模运算,当超过最大值( )后,会平滑地回绕到 0 重新开始。
如需进一步深入了解相关细节,推荐阅读 RFC 9293 规范中的 Sequence Numbers 章节。
上一章:第9章 源端口和目标端口 · 所属篇:第三篇 · 下一章:第11章 Acknowledgment Number · 教程总览