Skip to content

第14章 TCP Options

TCP 的固定首部(Header)只有 20 字节,选项(Options)字段为连接提供了扩展能力。在握手阶段,双方会在有限的空间里声明各自的接收能力并协商参数;连接建立后,有些选项还会持续用于传递测量数据或乱序信息。

选项区域如何编码

Data Offset 决定了整个 TCP 首部的长度。固定部分占 20 字节,剩余的最多 40 字节则是选项和填充数据(Padding)。TCP 首部的总长度必须对齐到 4 字节。

选项编码分为两种形式:

  • EOL 和 NOP 仅占 1 字节,即只有 Kind 字段;
  • 其他选项则采用 Kind - Length - Value 格式。注意,Length 字段表示选项的总长度(包含 Kind、Length 和 Value)。
Kind名称常见总长度典型出现阶段
0End of Option List(EOL)1标记有效选项结束,其后均为零填充
1No-Operation(NOP)1用于选项间的填充或对齐
2Maximum Segment Size(MSS)4SYN
3Window Scale(WS)3SYN
4SACK Permitted2SYN
5SACK2+8n连接建立后的 ACK 报文
8Timestamp10SYN 报文及协商成功后的数据报文

NOP 的主要作用是占位对齐,让后续的多字节字段落在理想的内存边界上,不过接收端依然会按字节逐个解析所有合法的排列。EOL 用于标记有效选项列表的结束;如果 Data Offset 划定的空间还未用完,剩下的字节全用 0 填充。

我们回到第三篇开头的抓包样本,它的选项区域正好是 12 字节:

text
01 01 | 08 0A 01 02 03 04 A0 B0 C0 D0

前两个 01 是 NOP,用于占位对齐。紧跟的 08 0A 表示 Kind 8、Length 10,即时间戳(Timestamp)选项。其后分别是 4 字节的发送端时间戳(TSval)0x01020304 和 4 字节的回显时间戳(TSecr)0xA0B0C0D0。选项部分共计 12 字节,加上 20 字节的固定首部,总首部长度为 32 字节,正好对应样本中 Data Offset 的值 8(即 8×4=32 字节)。这个例子也直观展示了 Timestamp 选项在连接建立后,是如何持续出现在数据段(Segment)中的。

手工解析一段合法的选项序列

下面是一段截取自 SYN 报文的 20 字节选项序列(竖线仅为方便阅读):

text
02 04 05 B4 | 04 02 | 08 0A 12 34 56 78 00 00 00 00 | 01 | 03 03 07

我们可以按 Kind 和 Length 依次解析:

  1. 02 04 05 B4:Kind 2,Length 4,MSS 值为 0x05B4(十进制 1460)。
  2. 04 02:Kind 4,Length 2,即 SACK Permitted 选项,告诉对端“我支持处理 SACK(选择性确认)”。
  3. 08 0A + 8 字节:Timestamp 选项。其中 TSval 为 0x12345678,因为这是初始的 SYN 报文,所以回显的 TSecr 全为 0。
  4. 01:一个 NOP 占位符。
  5. 03 03 07:Kind 3,Length 3,Window Scale 选项,表示窗口缩放因子为 7(即位移 s=7)。

20 字节固定首部加上这段 20 字节的选项,TCP 首部总长为 40 字节,因此 Data Offset 应该等于 40/4=10。这是一个排版非常规整的示例;在真实的抓包中,选项的排列顺序、NOP 的数量以及具体取值完全由各系统的 TCP 协议栈实现决定。

如果遇到不认识的未知选项,接收端会利用 Length 字段直接跳过它。RFC 9293 明确要求,TCP 协议栈必须能够接收报文段中的任意选项,并能根据 Length 安全跳过未知选项;如果解析出非法的长度值,则应进入错误处理逻辑。

MSS:告诉对端单段数据的接收上限

最大报文段长度(Maximum Segment Size, MSS)选项包含一个 16 位的数值。发送方通过它向对端声明:“我愿意接收的单段 TCP 载荷数据最大是多少”。该选项只会在带有 SYN 标志的报文段中出现,并且具有明确的方向性:

  • 客户端在 SYN 报文中宣告 MSS = 1460,服务端据此限制服务端发往客户端的 TCP 数据段大小。
  • 服务端在 SYN+ACK 报文中宣告 MSS = 1200,客户端据此限制客户端发往服务端的 TCP 数据段大小。

握手双方宣告的 MSS 值可以不一样。在实际发送数据时,TCP 协议栈会综合考虑本地网卡接口、路径 MTU(Path MTU)、拥塞控制(Congestion Control)算法、应用层实际要发送的数据量等多重因素,可能会选择比 MSS 更小的值来组装报文段。因此,MSS 只是一个接收能力的上限,在抓包时你看到的每一段真实数据长度(tcp.len)完全有可能低于这个值。

MSS 与 MTU 的计算关系

MTU(最大传输单元)约束的是网络层(IP 层)数据报在某段链路上的总尺寸,而 MSS 关注的纯粹是 TCP 层的数据载荷。对于最常见的以太网 MTU 1500,我们可以用 IP 和 TCP 的固定首部长度来估算 MSS:

MSSIPv4=150020IPv420TCP=1460MSSIPv6=150040IPv620TCP=1440

RFC 6691 明确规定:在计算 MSS 选项值时,只扣除 IP 和 TCP 的固定首部长度。如果实际发送的报文还额外携带了 TCP Timestamp 或 IP 层的其他选项,发送方会自行缩减该报文的数据载荷长度,以保证最终组装出的整个 IP 数据报依然能塞进 MTU 里。除此之外,VPN 隧道、IPv6 扩展首部以及路径 MTU(PMTU)的动态变化,都会直接影响最终发出的报文段大小。

提示:如果在发送端抓包,由于网卡的 TSO(TCP Segmentation Offload)或内核的 GSO 等分段卸载优化功能,你可能会在抓包软件里看到远超 MSS 限制的“超大 TCP 报文段”。这是正常现象,网卡硬件或底层软件会在数据真正上路前将其切片。我们会在第 32 章系统讲解这类由于网卡卸载导致的抓包“视觉差异”。

Window Scale:打破 16 位窗口的限制

TCP 首部中固定的 Window(窗口)字段只有 16 位,最大只能表示 65535 字节(64 KiB)。在当今的高速网络中,这往往成为吞吐量的瓶颈。Window Scale(窗口缩放)选项就是为了解决这个问题而诞生的。它携带一个缩放位移计数 s,在连接建立后,接收端会用这个位移因子将对端通告的窗口(Advertised Window)按以下公式放大:

Weffective=Wfield×2s

协议允许的最大位移为 14(即乘 214),这让 TCP 的有效接收窗口最大可达到约 1 GiB。Window Scale 选项仅在 SYN 报文中发送,一旦握手完成,每个方向的缩放因子就固定下来,直到连接结束。

关于窗口缩放的协商,有两个核心要点:

  1. 必须双向奔赴:只有客户端和服务端都在各自的 SYN 报文中携带了 Window Scale 选项,这条 TCP 连接才会真正启用窗口缩放功能。
  2. 各表各的位移:某端发送的位移 s,只用来缩放自己未来发出的 Window 字段。因为它宣告的永远是本端的接收能力。

假设我们的握手抓包记录如下:

报文原始 Window 字段值WS 选项位移含义
客户端 SYN642407客户端未来通告的窗口要乘以 27
服务端 SYN+ACK655358服务端未来通告的窗口要乘以 28

注意,SYN 和 SYN+ACK 报文中的 Window 字段总是表示原始的 16 位数值,不参与缩放。连接建立(ESTABLISHED 状态)后,假设客户端发出了一个包含 Window = 500 的报文,它实际是在向服务端宣告:“我现在还能接收 500×27=64000 字节的数据”。而如果服务端也发出了一个 Window = 500 的报文,它宣告的接收窗口则是 500×28=128000 字节。

在分析网络包时,Wireshark 通常会贴心地把原始值(Window size value)、缩放因子(Window size scaling factor)以及最终计算出的有效窗口(Calculated window size)一起显示出来。但在排查问题时,你需要特别注意:如果你的抓包文件没有包含最初的三次握手(比如从连接中途才开始抓取),Wireshark 就会因为不知道缩放因子而无法计算正确的窗口大小。此时它往往只能将计算结果标记为未知,或是使用一个并不准确的推断值。

SACK Permitted 与 SACK Blocks:精准定位丢包

传统的 TCP 累计确认(Cumulative Acknowledgment,即 Ack 字段)有一个致命弱点:它只能告诉发送方“在这之前的数据我都收到了”,也就是只能推到连续数据流的右边界。如果网络发生乱序或丢了几个中间的包,累计确认就无能为力了。

SACK(Selective Acknowledgment,选择性确认)机制正是为了解决这个问题。它允许接收方告诉发送方:“我已经收到了哪些不连续的数据块(区间)”,从而让发送方精准地只重传真正丢失的数据段。

第一步:在 SYN 握手中开启 SACK 能力

SACK Permitted 选项的长度只有 2 字节(包含 Kind 和 Length 自身)。发送这个选项的节点是在向世界宣告:“我具备解析 SACK 选项的能力,你可以把 SACK 发给我”。

举个例子:客户端准备向服务端上传大文件。服务端作为数据接收方,希望能在一部分报文段丢失(乱序)时,通过 SACK Blocks 把已收到的离散数据块报告给客户端。这有个前提:客户端必须在握手的 SYN 报文中发送了 SACK Permitted。现代操作系统通常会在三次握手时双向发送该选项,确保连接的两个方向都具备 SACK 的反馈能力。

第二步:用“半开区间”报告数据孤岛

一旦握手时启用了该能力,当接收方收到不连续的数据段时,就会在 ACK 报文中携带 SACK Blocks 选项。

每个 SACK Block 由两个 32 位的序列号(Sequence Number)组成:左边缘(Left Edge)和右边缘(Right Edge)。它们在数学上构成了一个左闭右开的区间,代表一段已经被成功接收的连续数据:

[LeftEdge,RightEdge)

假设当前接收方的累计确认 Ack = 1001,但它在后面又零散地收到了序列号区间为 [2001,3001)[4001,4501) 的两个数据段。那么这三个信息就可以像下面这样在接收队列中描绘出“数据孤岛”和“缺口”:

text
已连续接收的数据边界:  ... 1001
发现缺口(丢失或乱序):[1001,2001)
已收到的孤岛 (SACK 1): [2001,3001)
发现缺口(丢失或乱序):[3001,4001)
已收到的孤岛 (SACK 2): [4001,4501)

此时,报文首部中的累计 Ack 依然填 1001;但在选项区,SACK Blocks 明确告诉发送方这两块后续数据已经安全躺在接收缓存中了。

带有 2 个 Block 的 SACK 选项占用 2+8×2=18 字节。既然 TCP 选项区最多只有 40 字节,如果单独使用 SACK,理论上最多只能塞下 4 个 Block(2+8×4=34 字节);而在实际网络中,由于经常还要携带 12 字节的 Timestamp 选项及对齐填充,因此单个报文最常见容纳的 SACK Block 上限是 3 个。

需要强调的是,SACK 提供的信息只是一种“友情提示(Advisory)”。发送方可以利用这些提示优先重传填补缺口,但绝对不能盲目地清空重传缓冲区。真正能让发送方安全释放已发送数据的唯一标准,仍然是累计 Ack 的推进。 这种设计既大幅提升了多点丢包时的恢复效率,又坚守了 TCP 累计确认带来的稳定性。

Timestamp:更精准的 RTT 测量与防回绕机制

Timestamp(时间戳)选项固定占用 10 字节:

text
+--------+---------+--------------------+--------------------+
| Kind=8 | Len=10  | TSval (32位)       | TSecr (32位)       |
+--------+---------+--------------------+--------------------+
  • TSval (Timestamp Value):发送端自身维护的时间戳时钟值。它通常与时间单调递增相关,出于安全考虑,现代系统往往还会为每个连接加上一个随机偏移量。
  • TSecr (Timestamp Echo Reply):用于原封不动地“回显”之前从对端收到的 TSval。该字段仅当报文首部的 ACK 标志位置 1 时才有效。

在最初挥出的 SYN 报文中,由于尚未收到对端的任何信息(此时 ACK 未置位),按照规范发送端应将 TSecr 置为 0,接收端也会忽略该值。如果服务端同意启用时间戳选项,它会在回复的 SYN+ACK 中携带自己的 TSval,并在 TSecr 字段里回显客户端发来的值;客户端在第三次握手的 ACK 中,再同样回显服务端的 TSval。握手成功后(除了 RST 等少数例外情况),连接上的数据段将持续携带 Timestamp 选项。

Timestamp 主要承担两项核心任务:

  1. 更频繁的 RTT 测量(RTTM, Round-Trip Time Measurement):通过观察发出的 TSval 和对端回显的 TSecr,发送方能获得比传统机制多得多的往返时间样本,从而更精准地计算重传超时时间(RTO)。
  2. 防序列号回绕(PAWS, Protection Against Wrapped Sequences):在极高吞吐量和大窗口的网络中,32 位的序列号可能会在很短的时间内发生“回绕(Wrap-around)”,导致新旧数据段序列号重叠。接收方可以通过检查单调递增的时间戳来轻松识别出那些“迟到的历史幽灵报文”,直接将其丢弃。

注意区分两个时钟:TSval 的单位由操作系统的 TCP 实现自行决定(比如 1ms 或 10ms),它只适合用来做相对差值计算和简单的匹配。这与 Wireshark 数据包列表中显示的抓包相对时间(Time since first frame)完全是两码事。抓包时间轴属于网络监听工具的外部时钟,而 TSval 属于 TCP 端点内部的时钟,分析时千万不要将二者混为一谈。

四类常用选项的方向性总结

分析 TCP 选项时,最实用的思考方式是:先问 “谁发出了这个选项?”,再问 “它约束的是哪个数据流方向?”

选项发送方在表达什么接收方应该怎么做
MSS我单次最多只愿意接收这么多 TCP 载荷严格限制发往该方向的报文段数据长度
Window Scale我未来通告的 Window 字段都要按这个系数放大接收到该端发来的窗口通告时进行解码计算
SACK Permitted放心,我能解析你将来发给我的 SACK 区间当出现丢包或乱序时,放心地向该端报告数据孤岛
Timestamp这是我当前的时钟值,顺便回显一下你之前的时钟值利用它采集 RTT 样本,并在高速网络中防御序列号回绕

记住:抓包分析时,客户端的 SYN 和服务端的 SYN+ACK 必须拆开独立看待,千万不要把一端的数值强加给另一端,否则你计算出的 MSS 上限和接收窗口将南辕北辙。

受控实验:对比解析 SYN 与 SYN+ACK

我们在 Npcap Loopback Adapter(本地回环网卡)上开启 Wireshark 抓包。接着,在终端 A 启动一个简单的本地 HTTP 服务:

powershell
python -m http.server 39014 --bind 127.0.0.1

在终端 B 发起一次 HTTP/1.1 请求来触发 TCP 建立连接:

powershell
curl.exe --http1.1 http://127.0.0.1:39014/ -o NUL

请求完成后,停止抓包。在 Wireshark 中输入以下过滤器,只看握手报文:

text
tcp.port == 39014 && tcp.flags.syn == 1

现在,请你分别展开客户端发出的 SYN 报文和服务端回复的 SYN+ACK 报文的 Options 树,尝试自己填写下表:

数据流方向Data OffsetMSSWS shiftSACK PermittedTSvalTSecr
客户端 → 服务端
服务端 → 客户端

填完之后,请动手完成这四个核对动作:

  1. 通过你提取的 Data Offset 计算出选项的总长度,并与 Wireshark 界面上各个选项及 Padding 字节加起来的总和进行对比。
  2. 试着从不同视角解读 MSS:客户端宣告的值限制了服务端的发送量,服务端宣告的值限制了客户端的发送量。
  3. 如果双方都启用了 Window Scale,请在建立连接后的报文中随意找两个 ACK,根据它们所在方向的位移值,手工计算一下实际的有效接收窗口。
  4. 如果双方都启用了 Timestamp,请仔细比对 SYN+ACK 中的 TSecr 是否等于客户端 SYN 中的 TSval,随后再检查第三次握手的 ACK 是否正确回显了服务端的 TSval。

在健康的本地回环测试中,数据传输通常非常顺畅,因此你往往只会看到宣告能力的 SACK Permitted,而极难见到真正的 SACK Blocks(因为没有丢包)。你可以先通过前文的区间例子在纸面上掌握解码逻辑;等到了第 21 章,我们会在受控模拟的弱网环境中,亲眼观察 SACK Blocks 是如何随着丢包缺口动态生成的。

最后需要说明的是,针对本机回环接口,操作系统给出的 MSS 往往大得惊人(接近 64 KiB),选项的排列顺序也未必和前文教科书般的示例一致。但这正是真实的工程现场——这些参数原原本本地反映了你当前电脑的网卡和协议栈行为。我们这次实验的终极目标,就是让你亲手拆解结构、明确方向性、跑通窗口缩放的数学计算,并彻底看懂时间戳的“抛接球”游戏。

理解检查

  1. 客户端在 SYN 中宣告 MSS 为 1460,服务端在 SYN+ACK 中宣告 MSS 为 1200。当客户端向服务端发送数据时,它组装的数据段大小主要受哪个值的约束?
  2. 客户端在握手时宣告了 Window Scale 缩放因子(shift)为 7。在连接建立后的传输中,客户端发出了一个原始 Window 字段值为 2000 的报文,它宣告的有效接收窗口是多少?
  3. 你看到某个报文的累计 Ack = 5001,同时带有一个 SACK 选项 [6001,7001)。请问对于接收方来说,目前缺失的、最靠前的序列号缺口(区间)是什么?
  4. 如果一个 SACK 选项的 Length 字段等于 26,它里面包含多少个 Block?
  5. 当报文首部的 Data Offset 等于 11 时,TCP 选项区(包含对齐填充的 Padding)总共占用了多少字节?
  6. 在以太网 MTU 为 1500,且 IPv4 和 TCP 均只使用 20 字节固定首部时,MSS 选项的典型值是多少?如果某个实际发送的数据段还带了 12 字节的 TCP 选项,发送方应当如何调整以确保 IP 报文依然不超过 MTU 的限制?
参考答案
  1. 受服务端宣告的 1200 约束。因为它是服务端发给客户端的接收能力上限。
  2. 有效窗口为 2000×27=256000 字节。
  3. 缺失的最前方缺口为 [5001,6001)
  4. 包含 3 个 Block。计算公式:(262)/8=3
  5. 总首部长为 11×4=44 字节,扣除 20 字节固定部分,选项与填充共占 24 字节。
  6. 典型 MSS 值为 15002020=1460 字节。需要注意,MSS 声明值是基于固定首部估算的上限;当特定报文携带了 12 字节的 TCP 选项时,TCP 协议栈会在组装报文时主动削减 12 字节的载荷数据(即最大载荷变为 1448),从而确保算上选项后的总长度依然不会超过 1500 的 MTU。

本章小结

  • 选项的 KindLength 结构让接收端可以按序逐项解析,而首部的 Data Offset 则划定了选项与填充字节的总边界。
  • MSS(最大报文段长度) 限制了对端发往该宣告方的 TCP 载荷数据大小;不要和网络层的 MTU 混淆。
  • Window Scale(窗口缩放) 选项必须通过握手双向声明,每一端宣告的位移因子,只用于放大该端后续通告的窗口(Window)字段。
  • SACK Permitted 选项在 SYN 阶段表明本端处理乱序区间的资质;SACK Blocks 则采用左闭右开区间报告连续确认号(Ack)之后收到的“数据孤岛”。
  • Timestamp(时间戳) 选项依靠 TSval 和 TSecr 支持更密集的 RTT 测量(RTTM)和抵御序列号回绕(PAWS)。注意区分端点内部时钟与外部抓包软件的时间轴。
  • TCP 握手阶段的宣告均具有强烈的方向性,在排查问题时,务必将客户端的 SYN 与服务端的 SYN+ACK 拆开独立分析。

规范依据


上一章:第13章 固定字段 · 所属篇:第三篇 · 下一章:第15章 三次握手 · 教程总览

Licensed under CC BY-NC-SA 4.0.