Appearance
第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 | 名称 | 常见总长度 | 典型出现阶段 |
|---|---|---|---|
| 0 | End of Option List(EOL) | 1 | 标记有效选项结束,其后均为零填充 |
| 1 | No-Operation(NOP) | 1 | 用于选项间的填充或对齐 |
| 2 | Maximum Segment Size(MSS) | 4 | SYN |
| 3 | Window Scale(WS) | 3 | SYN |
| 4 | SACK Permitted | 2 | SYN |
| 5 | SACK | 连接建立后的 ACK 报文 | |
| 8 | Timestamp | 10 | SYN 报文及协商成功后的数据报文 |
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(即
手工解析一段合法的选项序列
下面是一段截取自 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 依次解析:
02 04 05 B4:Kind 2,Length 4,MSS 值为0x05B4(十进制 1460)。04 02:Kind 4,Length 2,即 SACK Permitted 选项,告诉对端“我支持处理 SACK(选择性确认)”。08 0A+ 8 字节:Timestamp 选项。其中 TSval 为0x12345678,因为这是初始的 SYN 报文,所以回显的 TSecr 全为 0。01:一个 NOP 占位符。03 03 07:Kind 3,Length 3,Window Scale 选项,表示窗口缩放因子为 7(即位移 )。
20 字节固定首部加上这段 20 字节的选项,TCP 首部总长为 40 字节,因此 Data Offset 应该等于
如果遇到不认识的未知选项,接收端会利用 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:
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(窗口缩放)选项就是为了解决这个问题而诞生的。它携带一个缩放位移计数
协议允许的最大位移为 14(即乘
关于窗口缩放的协商,有两个核心要点:
- 必须双向奔赴:只有客户端和服务端都在各自的 SYN 报文中携带了 Window Scale 选项,这条 TCP 连接才会真正启用窗口缩放功能。
- 各表各的位移:某端发送的位移
,只用来缩放自己未来发出的 Window 字段。因为它宣告的永远是本端的接收能力。
假设我们的握手抓包记录如下:
| 报文 | 原始 Window 字段值 | WS 选项位移 | 含义 |
|---|---|---|---|
| 客户端 SYN | 64240 | 7 | 客户端未来通告的窗口要乘以 |
| 服务端 SYN+ACK | 65535 | 8 | 服务端未来通告的窗口要乘以 |
注意,SYN 和 SYN+ACK 报文中的 Window 字段总是表示原始的 16 位数值,不参与缩放。连接建立(ESTABLISHED 状态)后,假设客户端发出了一个包含 Window = 500 的报文,它实际是在向服务端宣告:“我现在还能接收 Window = 500 的报文,它宣告的接收窗口则是
在分析网络包时,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)。它们在数学上构成了一个左闭右开的区间,代表一段已经被成功接收的连续数据:
假设当前接收方的累计确认 Ack = 1001,但它在后面又零散地收到了序列号区间为
text
已连续接收的数据边界: ... 1001
发现缺口(丢失或乱序):[1001,2001)
已收到的孤岛 (SACK 1): [2001,3001)
发现缺口(丢失或乱序):[3001,4001)
已收到的孤岛 (SACK 2): [4001,4501)此时,报文首部中的累计 Ack 依然填 1001;但在选项区,SACK Blocks 明确告诉发送方这两块后续数据已经安全躺在接收缓存中了。
带有 2 个 Block 的 SACK 选项占用
需要强调的是,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 主要承担两项核心任务:
- 更频繁的 RTT 测量(RTTM, Round-Trip Time Measurement):通过观察发出的 TSval 和对端回显的 TSecr,发送方能获得比传统机制多得多的往返时间样本,从而更精准地计算重传超时时间(RTO)。
- 防序列号回绕(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 Offset | MSS | WS shift | SACK Permitted | TSval | TSecr |
|---|---|---|---|---|---|---|
| 客户端 → 服务端 | ||||||
| 服务端 → 客户端 |
填完之后,请动手完成这四个核对动作:
- 通过你提取的 Data Offset 计算出选项的总长度,并与 Wireshark 界面上各个选项及 Padding 字节加起来的总和进行对比。
- 试着从不同视角解读 MSS:客户端宣告的值限制了服务端的发送量,服务端宣告的值限制了客户端的发送量。
- 如果双方都启用了 Window Scale,请在建立连接后的报文中随意找两个 ACK,根据它们所在方向的位移值,手工计算一下实际的有效接收窗口。
- 如果双方都启用了 Timestamp,请仔细比对 SYN+ACK 中的 TSecr 是否等于客户端 SYN 中的 TSval,随后再检查第三次握手的 ACK 是否正确回显了服务端的 TSval。
在健康的本地回环测试中,数据传输通常非常顺畅,因此你往往只会看到宣告能力的 SACK Permitted,而极难见到真正的 SACK Blocks(因为没有丢包)。你可以先通过前文的区间例子在纸面上掌握解码逻辑;等到了第 21 章,我们会在受控模拟的弱网环境中,亲眼观察 SACK Blocks 是如何随着丢包缺口动态生成的。
最后需要说明的是,针对本机回环接口,操作系统给出的 MSS 往往大得惊人(接近 64 KiB),选项的排列顺序也未必和前文教科书般的示例一致。但这正是真实的工程现场——这些参数原原本本地反映了你当前电脑的网卡和协议栈行为。我们这次实验的终极目标,就是让你亲手拆解结构、明确方向性、跑通窗口缩放的数学计算,并彻底看懂时间戳的“抛接球”游戏。
理解检查
- 客户端在 SYN 中宣告 MSS 为 1460,服务端在 SYN+ACK 中宣告 MSS 为 1200。当客户端向服务端发送数据时,它组装的数据段大小主要受哪个值的约束?
- 客户端在握手时宣告了 Window Scale 缩放因子(shift)为 7。在连接建立后的传输中,客户端发出了一个原始 Window 字段值为 2000 的报文,它宣告的有效接收窗口是多少?
- 你看到某个报文的累计
Ack = 5001,同时带有一个 SACK 选项[6001,7001)。请问对于接收方来说,目前缺失的、最靠前的序列号缺口(区间)是什么? - 如果一个 SACK 选项的 Length 字段等于 26,它里面包含多少个 Block?
- 当报文首部的 Data Offset 等于 11 时,TCP 选项区(包含对齐填充的 Padding)总共占用了多少字节?
- 在以太网 MTU 为 1500,且 IPv4 和 TCP 均只使用 20 字节固定首部时,MSS 选项的典型值是多少?如果某个实际发送的数据段还带了 12 字节的 TCP 选项,发送方应当如何调整以确保 IP 报文依然不超过 MTU 的限制?
参考答案
- 受服务端宣告的 1200 约束。因为它是服务端发给客户端的接收能力上限。
- 有效窗口为
字节。 - 缺失的最前方缺口为
。 - 包含 3 个 Block。计算公式:
。 - 总首部长为
字节,扣除 20 字节固定部分,选项与填充共占 24 字节。 - 典型 MSS 值为
字节。需要注意,MSS 声明值是基于固定首部估算的上限;当特定报文携带了 12 字节的 TCP 选项时,TCP 协议栈会在组装报文时主动削减 12 字节的载荷数据(即最大载荷变为 1448),从而确保算上选项后的总长度依然不会超过 1500 的 MTU。
本章小结
- 选项的 Kind 和 Length 结构让接收端可以按序逐项解析,而首部的 Data Offset 则划定了选项与填充字节的总边界。
- MSS(最大报文段长度) 限制了对端发往该宣告方的 TCP 载荷数据大小;不要和网络层的 MTU 混淆。
- Window Scale(窗口缩放) 选项必须通过握手双向声明,每一端宣告的位移因子,只用于放大该端后续通告的窗口(Window)字段。
- SACK Permitted 选项在 SYN 阶段表明本端处理乱序区间的资质;SACK Blocks 则采用左闭右开区间报告连续确认号(Ack)之后收到的“数据孤岛”。
- Timestamp(时间戳) 选项依靠 TSval 和 TSecr 支持更密集的 RTT 测量(RTTM)和抵御序列号回绕(PAWS)。注意区分端点内部时钟与外部抓包软件的时间轴。
- TCP 握手阶段的宣告均具有强烈的方向性,在排查问题时,务必将客户端的 SYN 与服务端的 SYN+ACK 拆开独立分析。
规范依据
- RFC 9293:Transmission Control Protocol (TCP),定义通用选项格式、EOL、NOP 与 MSS。
- RFC 6691:TCP Options and Maximum Segment Size (MSS),澄清 MSS 与 IP/TCP 选项长度的计算关系。
- RFC 7323:TCP Extensions for High Performance,定义 Window Scale、Timestamp、RTTM 与 PAWS。
- RFC 2018:TCP Selective Acknowledgment Options,定义 SACK Permitted 与 SACK Blocks。