4. MSS 与 MTU:最大报文段长度、最大传输单元、分片与重组

聊到 TCP 粘包拆包,有个绕不开的话题就是 MSS 和 MTU。很多新手搞混这两个概念,甚至以为它们是一回事。我刚开始做网络开发时也踩过这个坑——明明应用层发了 5000 字节的数据,对端死活收不全,抓包一看,中间被路由器切成了好几片。

说白了,MTU 是链路层的限制,MSS 是 TCP 层的协商结果。它们之间的关系,决定了你的数据在网络里到底怎么走。

4.1 MTU:最大传输单元

MTU(Maximum Transmission Unit),指的是网络接口层能承载的最大数据包大小。以太网的标准 MTU 是 1500 字节。这个值不是随便定的,跟硬件、传输介质都有关系。

你想想看,如果 IP 层要发一个 3000 字节的包,网卡一次只能发 1500 字节,怎么办?分片。IP 层会把大包切成多个小片,每个片单独发送,到达目的地后再重组。

关键点:MTU 是 IP 层的概念,它决定了 IP 数据报的最大长度。超过 MTU 的 IP 包必须分片。

我记得有一次排查一个跨机房传输慢的问题,两边都是千兆网卡,但中间经过了一个 VPN 隧道。隧道封装后包变大了,超过了 1500,结果大量分片导致性能骤降。后来把 MTU 调成 1400,问题就解决了。

4.2 MSS:最大报文段长度

MSS(Maximum Segment Size),是 TCP 层的一个概念。它表示 TCP 数据段中实际载荷的最大长度,不包括 TCP 头部和 IP 头部。

MSS 是怎么来的?在 TCP 三次握手的时候,双方会交换 MSS 选项。计算公式很简单:

MSS = MTU - IP头部长度 - TCP头部长度

以标准以太网为例:

MSS = 1500 - 20 (IP头) - 20 (TCP头) = 1460 字节

如果启用了 TCP 时间戳等选项,TCP 头部会变成 32 字节,MSS 就变成:

MSS = 1500 - 20 - 32 = 1448 字节
我的习惯:在抓包分析时,先看 SYN 包里的 MSS 选项。如果 MSS 小于 1460,说明中间可能有隧道或 VPN 设备在干预。这是排查路径 MTU 问题的第一线索。

4.3 分片与重组

分片发生在 IP 层,重组发生在接收端的 IP 层。这里有个容易混淆的点:TCP 层不做分片,TCP 只负责把数据切成 MSS 大小的段,然后交给 IP 层。如果 IP 层发现包太大,它自己会分片。

分片的细节:

  • 每个 IP 分片都有相同的 Identification 标识符
  • 通过 Fragment Offset 字段记录偏移量
  • More Fragments (MF) 标志位表示是否还有后续分片

举个例子,假设 TCP 层交下来一个 3000 字节的数据段(含 TCP 头),加上 IP 头 20 字节,总共 3020 字节。以太网 MTU 1500,需要分片:

分片 数据长度 偏移量 MF 标志
第 1 片 1480 字节 0 1
第 2 片 1480 字节 1480 1
第 3 片 60 字节 2960 0

注意,每个分片都包含 IP 头,但只有第一个分片包含 TCP 头。如果第一个分片丢了,接收端无法解析 TCP 端口号,整个包都得重传。

避坑指南:我曾经遇到过一个诡异的问题——某个 HTTP 请求偶尔超时,抓包发现 TCP 三次握手正常,但数据包被分片后,中间某个分片被防火墙丢弃了。因为防火墙只检查第一个分片的 TCP 头,后续分片没有端口信息,被当成非法包拦截了。解决方案是让应用层把数据控制在 MSS 以内,避免分片。

4.4 MSS 与 MTU 的关系图

下面这张图展示了从应用层数据到物理帧的完整封装过程,以及 MSS 和 MTU 的边界:

MSS 与 MTU 封装层次关系图 应用层数据(例如 HTTP 请求体) TCP 头部(20-32 字节) + 应用层数据 = TCP 段 MSS 边界 IP 头部(20 字节) + TCP 段 = IP 数据报 以太网头部(14 字节) + IP 数据报 + 尾部(4 字节)= 以太网帧 MTU 边界 分片场景说明 • 当 IP 数据报大小 > MTU(1500 字节)时,IP 层进行分片 • 每个分片包含 IP 头,但只有第一个分片包含 TCP 头 • 接收端根据 Identification + Fragment Offset + MF 标志重组 • 任何一片丢失,整个 IP 数据报都需要重传(由 TCP 层触发) 典型值:MTU=1500 → MSS=1460(标准 TCP 头)或 MSS=1448(含时间戳)

4.5 路径 MTU 发现

现实网络里,两端之间的路径上可能有不同的链路,MTU 不一定都是 1500。比如 PPPoE 拨号会把 MTU 降到 1492,某些 VPN 隧道可能更低。

TCP 协议有个机制叫 Path MTU Discovery (PMTUD),它通过发送设置了 Don't Fragment (DF) 标志的 IP 包来探测路径上的最小 MTU。如果中间路由器发现包太大且 DF 置位,就会返回一个 ICMP 消息:Fragmentation Needed,并告诉发送方该链路的 MTU 值。

我曾经遇到过:某次线上服务间歇性卡顿,抓包发现大量 ICMP "Fragmentation Needed" 消息,但发送方没收到——因为中间防火墙把 ICMP 给过滤了。结果发送方一直重传大包,永远发不过去。这就是经典的 PMTUD 黑洞问题。解决办法是在应用层手动设置 TCP_MSS 或调整网卡 MTU。

4.6 粘包拆包与 MSS 的关系

回到粘包拆包的话题。MSS 决定了 TCP 层一次最多能发多少数据。如果应用层一次写入的数据超过 MSS,TCP 会分多次发送。这本身不是粘包,但会影响接收方的处理逻辑。

举个例子:

  • 应用层发送 3000 字节的 JSON 数据
  • TCP 层按 MSS=1460 切分成 3 个段:1460 + 1460 + 80
  • 接收方可能一次 recv 读到 1460 字节,也可能一次读到 3000 字节(如果网络延迟低,多个段同时到达)

这就是粘包拆包的根源之一——应用层不知道 TCP 段边界在哪里。MSS 只是 TCP 层的分段单位,应用层必须自己定义消息边界。

核心结论:MSS 影响的是 TCP 层怎么切数据,但应用层粘包拆包的问题,必须通过应用层协议来解决(比如固定长度、分隔符、TLV 等)。不要指望 MSS 帮你做消息边界划分。

4.7 抓包实战:观察 MSS 和分片

用 Wireshark 抓包时,怎么看 MSS 和分片?

  1. 查看 MSS 协商:找到三次握手的 SYN 包,展开 TCP 选项,找到 "Maximum Segment Size" 字段。客户端和服务器各自声明自己的 MSS。
  2. 观察分片:在 IP 层,如果看到多个 IP 包有相同的 Identification 值,且 Fragment Offset 不为 0,说明发生了分片。
  3. DF 标志:如果 IP 头部的 Flags 字段显示 "Don't fragment" 置位,说明发送方启用了 PMTUD。

我习惯在 Wireshark 里加一列 "MSS",方便快速查看每个 TCP 会话的 MSS 值。操作很简单:右键列头 → Column Preferences → 添加 "tcp.options.mss_val"。

注意:分片对性能影响很大。每个分片都需要独立的 IP 头,而且只要一个分片丢失,整个数据报就得重传。在局域网里问题不大,但在公网上,分片丢失率会显著增加。所以,尽量让应用层数据小于 MSS,避免 IP 分片。

嗯,MSS 和 MTU 这块内容就讲到这里。记住一句话:MTU 是链路层的硬限制,MSS 是 TCP 层的软协商。理解了它们的关系,你就能更好地分析网络传输中的各种奇怪现象了。