先问一个也许你早就好奇过的问题:我们明明把一条 TCP 连接的数据包得好好的,为什么到了数据链路层,一个"完整"的 IP 报文还会被切成几段,跑到半路又拼回去?

答案要从"路"说起。网络不是只有一根管子,一个 IP 报文要从源主机到达目的主机,中途可能要经过一个又一个路由器,每一跳连接它们的物理介质可能千差万别:有的车厢宽敞,有的车厢狭窄。就像一条快递物流链,前端能装大箱子,途中某段支线却只收小包裹——你总不能因为过不去就扔掉货物,于是只能拆箱分装,到终点再拼回去。在 IP 的世界里,这个"拆箱分装"的动作就叫分片,终点"拼回去"的动作就叫重组(reassembly)。

这篇文章我们就把它彻底掰开:为什么分片、靠哪三个字段识别、片偏移为什么必须用 8 字节做单位、一个真实的报文怎么一步步被切开、目的主机又怎么按序拼回,以及 DF 位、路径 MTU、UDP/TCP/ICMP 与分片之间那些容易踩的坑。全程会用一张张图和一个完整算例陪着你,最后的思考题也会给全详解答案。

你应该有的知识准备

在往下走之前,只要你对下面这两个概念有点印象就够了:

  • IP 头部与载荷:一个 IP 数据报由"IP 头部"和"载荷"组成。IPv4 头部通常是 20 字节(不含选项),载荷就是真正要交给上层协议(TCP/UDP/ICMP)的数据。分片切的是"载荷",但每片都要带上自己的 IP 头。
  • 数据链路层的载荷上限:IP 层把数据报送给哪张网卡,网卡(数据链路层)不是无限的,它有自己的"一次最多能装多少 IP 数据"的限制。这个限制正是分片的触发器。

为什么要分片?从 MTU 说起

分片不是 IP 想找事,而是被MTU逼出来的。MTU(Maximum Transmission Unit,最大传输单元)指数据链路层在对 IP 层数据报进行封装时,所能接受的最大数据长度。通俗讲:某段物理链路(以太网、WLAN、PPP 等每种介质一份)允许"单个数据帧里装多少字节的 IP 数据"是有一个上限的,这个上限就是这条链路的 MTU。

举几个典型的数:

链路/协议典型 MTU
以太网(Ethernet)1500 字节
点对点协议 PPP1492 / 1500 等
FDDI4352
环回接口 lo65536 左右(本质不受限)
一些隧道/GRE1460 / 1472 等更小

注意,MTU 指的是数据链路层能装多少"IP 数据",通常就用来比较"IP 数据报的总长度"(含 IP 头)。

当 IP 层要发送一个数据报,发现它的总长度超过了出接口的 MTU,它就有两个选择:要么拆,要么丢。默认拆——这就是分片出现的原因。换句话说,分片遵循的是"外层不迁就、内层主动适配":数据链路层铁板一块地把能装的数据装死在一定大小,超出的 IP 数据报只能内部自行切成小份。

这里还要澄清一个常见的直觉误区:分片并不是"把数据报切成刚好等于 MTU 大小的几片"这么简单,因为每一片都得再额外带上一个 20 字节的 IP 头部。所以一片"只能装 MTU 减掉 20 字节的载荷",这个细节我们在算例里会看到它是决定一切的关键。

三驾马车:标识、标志片、片偏移

要正确切分、还要让对端能拼回,IP 头部里有三个字段配合搞定了这件事。它们长这样(IPv4 头部第二行):

 0                   1                   2                   3
 0 1 2 3 4 5 6 7  8 9 0 1 2 3 4 5  6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|     Fragment Offset      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第二行从左到右就是我们要重点解读的三个家伙。请注意,Flags 是 3 位,Fragment Offset(片偏移)是 13 位,它们挤在同一个 16 位的位置上。

16 位标识(Identification)

标识(Identification) 是一个 16 位的编号,由发送 IP 报文的源主机生成,用于唯一地区分主机发出的不同数据报。它最重要的作用是这样:如果某个 IP 数据报在数据链路层被分片了,那么属于同一个原始数据报的所有片,它们的标识字段的值是完全相同的。

你可能会追问:"一个 16 位的数,久了回绕了怎么办?"没错,标识是有限的,发到 65535 会回绕,但它只要在"这片数据还在网络里存活的那段时间"保持唯一就够用了。真正把它"识别为一个整体"的,其实是"源 IP + 目的 IP + 协议 + 标识"这四元组:这四者完全相同的片,才被认为是同一个原始数据报的,后续会一起参与重组。

标志位:保留位、DF、MF

Flags 标志位 一共 3 位,它们的安排是:

  • 第 1 位:保留位(Reserved),必须为 0。保留的意思是现在不用,但还没想好说不定以后要用,先占着。
  • 第 2 位:DF(Don't Fragment,禁止分片)。置 1 表示"我不允许这个数据报被分片"。如果这时候数据报总长度又超过了 MTU,IP 模块不会去切,而是直接丢弃它,并(在合适的场合)回一个 ICMP 报错,告诉发送端"我过不去,而且你不让切"。这一位是后面"路径 MTU 发现"和"MTU 黑洞"故事的重量级主角。
  • 第 3 位:MF(More Fragments,更多分片)。如果报文被分片了,最后一个分片这一位置 0,表示"我是最后一片,后面没有了";其余分片都置 1,表示"我后面还有片"。它就像一个结束标记,接收方靠它判断"是不是还有一个尾巴没到"。

三个可以这样速记:DF 管"能不能切",MF 管"是不是最后",保留位就乖乖躺着。

片偏移(Fragment Offset)

片偏移(Fragment Offset) 是用来说明"我这一片在原始数据报里的哪个位置"的 13 位字段。这里有一个全篇最重要的规定,先背下来:

片偏移字段的值,表示本片的数据在原始数据报数据区中的偏移量,单位是"8 字节"。

也就是说,片偏移字段里的数值,并不是这个位置本身的字节号,而是要乘以 8 才是真实的字节偏移。比如片偏移字段写的是 185,那它拼起来之后要放在原始数据区第 185 × 8 = 1480 字节往后。反过来套用一下源材料里那句易混的话:片偏移字段的数值 = 相对起始字节数 ÷ 8,等价于真实字节偏移 = 字段数值 × 8。

为什么片偏移必须用"8 字节"做单位?

你可能已经嗅到不对劲了:为什么要多此一举地"乘 8"而不是直接写字节号?原因很朴素——位数不够用。

分片偏移只有 13 位,13 位二进制能表达的范围是 0 到 8191,也就是如果你按"1 字节"为单位,最多只能描述到第 8191 字节的偏移。但是 IPv4 报文的"总长度"可是 16 位字段,最大能到 65535 字节。试想一个 40000 字节的数据报被切成一堆 1480 字节的片,最后一片的偏移早就超过 8191 了——13 位根本记不下了。

怎么办?标准委员会选择了"以精度换范围":把偏移的单位放大到 8 字节。这样一来,13 位最大能表达的对象就变成了 8191 × 8 = 65528 字节附近,正好覆盖到 IPv4 数据报的最大范围(65535 字节量级)。这就是"偏移必须是 8 的整数倍"这一整套约束的根源——它是一个为把 16 位总长度塞进 13 位偏移而做的数学取舍。

由此连带出了三个"硬规矩",都是同一个原因的下游结论:

  1. 片偏移字段的值,必须是 8 的整数倍对应的字节偏移——你说到底,就是"真实字节偏移必须是 8 的倍数"(因为偏移字段是 8 为单位)。
  2. 除了最后一片,其余每一片的长度都必须是 8 的整数倍。 为什么?因为下一片的偏移 = 上一片起点 + 上一片长度,要做到下一片的偏移还是 8 的倍数,上一片的长度就必须是 8 的倍数,这样偏移才能连续接上、中间不出现空洞又不错位。
  3. 只有最后一片可以不是 8 的倍数。 因为它是尾部,后面不再跟别的片来对齐偏移,剩下的字节直接收尾即可。还记得源材料里那句话吗——"除了最后一个报文之外,其他报文的长度必须是 8 的整数倍,否则报文就不连续了"。正是这个意思。

这两个"8"(单位是 8、长度是 8 的倍数)是最容易翻车的点,下面的算例会让你彻底看清楚它俩是怎么联动的。

分片全过程:一个 3000 字节报文的归根算例

好,纸上谈兵够了,来一场实打实的分割。就按源材料最后的那个问题来:假设在 IP 层,有一个总长度为 3000 字节的报文,环境是标准的以太网 MTU = 1500 字节,我们要把它分片,并给出每片的完整参数。

先算清楚"一片能装多少"这个根基。IP 头 20 字节,所以每一片能携带的最大载荷是:

每片最大载荷 = MTU - IP头部长度
             = 1500 - 20
             = 1480 字节

注意检查:1480 ÷ 8 = 185,正好整除,是 8 的倍数,满足"中间片必须 8 的倍数"的硬规矩,不用向下取整,完美。

下面对原始报文做"数据区定坐标"。3000 字节里,20 字节是 IP 头,真正的数据区是 [0, 2979],共 2980 字节。用图表示整个报文:

原始 IP 数据报(总长 3000 字节)
+----------------+----------------------------------------+
|   IP 头部 20B  |            数据区 2980 字节            |
+----------------+----------------------------------------+
          起始[0]                                    [2979]

现在动手切。第二节片装 1480 字节数据(前提:这是一个 8 的倍数):

片1   [0 , 1479]   1480 字节
        ^                         偏移字段 = 0/8  =    0
片2   [1480, 2959] 1480 字节
                   ^              偏移字段 = 1480/8 = 185
片3   [2960, 2979]   20 字节
                             ^    偏移字段 = 2960/8 = 370

把每片的参数整理成一张表格:

分片覆盖数据区间数据长度片偏移字段总长度(头+数据)MFDF
片10 ~ 14791480020 + 1480 = 15001(后面还有)0
片21480 ~ 295914801480/8 = 18520 + 1480 = 15001(后面还有)0
片32960 ~ 2979202960/8 = 37020 + 20 = 400(我是最后一片)0

逐条核对,看看每个数字都对得上:

  1. 连续性:片1 起点 0、片2 起点 1480、片3 起点 2960,正好首尾相接,中间没有空洞也没有重叠。这就是"上一片长度是 8 的倍数、下一片偏移就对得齐"的直接体现。
  2. 偏移字段:分别乘以 8 得到 0、1480、2960,与真实字节起点一致。
  3. 最后一片的非 8 倍:片3 的数据长 20 字节,20 ÷ 8 = 2.5 不是整数倍——没关系,它是最后一片,允许。前面 1480 才是 8 的倍数。这正是源材料所说"最后一片可以例外"的活例子。
  4. MF 链:片1、片2 的 MF = 1(告诉对端"别急,还有"),片3 的 MF = 0("到此为止")。对端正是靠"收到一片 MF=0,且前面的偏移都补上了"来判断收齐的。

再画一张更直观的"三段拼回"示意图,方便你在脑子里把三片拼成一整块:

 片1(off=0)   片2(off=185)     片3(off=370,MF=0)
+----------+  +----------+  +------+
|1480 数据 |  |1480 数据 |  |20 数据|
+----------+  +----------+  +------+
    [0,1479]      [1480,2959]    [2960,2979]
        \             |             /
         \            |            /
          \           |           /
           +--------------------+
           | 原始数据区 2980 字节 |
           +--------------------+

到这里你应该已经能回答"如何分片"了:定最大载荷、切成 8 的倍数块、算好每个偏移字段、给每片补上 20 字节 IP 头、MF 打标。但看官留意——把三片从"物理上分割"变成"逻辑上连续可读",是整个分片里最沉重的一步:我们原本只传输 3000 字节,如今三片总共传输了 1500 + 1500 + 40 = 3040 字节,多出来的 40 字节,正是那三个额外 IP 头的开销。片越多,浪费越大。这是分片受诟病的原因之一,下文还会再提。

那么假设你的报文是 3500 字节,MTU 还是 1500?数据区为 3500 - 20 = 3480 字节,应切成 1480 + 1480 + 520(即 3480 = 1480 + 1480 + 520)。片3 的数据是 520 字节、偏移字段为 2960 / 8 = 370、总长 520 + 20 = 540。你不妨自己把片1、片2、片3 的"数据区间、数据长度、偏移字段、总长度、MF"这五样都推一遍,再拿"每片数据是 8 的倍数、偏移乘 8、总长等于头加数据"这三条规则逐一自查——留给你做二验。

组装全程:目的主机如何拼回原报文

分片在中间每一跳做完,但重组一般只在目的主机进行。也就是说,中继路由器拿到每片,直接照常往前送,不负责拼;只有到达最终目的地,才轮到 IP 层把这些碎片拼回一个完整的原始数据报。既然如此,我们就站在目的主机的 IP 层,看它怎么完成源材料那三步提问:如何得知报文被分片了?如何得知分片收全了?如何组成完整报文?

步骤一:识别"这些片是一家的"

目的主机 IP 层收到一个 IP 数据报,先看 Flag/MF 和 Offset:

  • 如果 MF=0 且 Offset=0(通常总长度也不超过 MTU),那这就是一个未被分片的普通数据报,直接交给上层,一拍即合。
  • 如果 MF=1,或者 Offset>0(哪怕 MF=0,只要偏移不为 0 就说明它前面还有片),就断定"这是某个数据报的一个分片,我要把它收进重组缓冲区"。

每个分片进来后,目的主机用 (源 IP、目的 IP、协议、标识 ID) 这个四元组去"对号入座"——同一个四元组的片,进同一个重组合集。这就是源材料里说的"根据标识字段将属于同一个数据报的所有分片挑选出来"。

步骤二:用片偏移给分片排序、填进格子

一旦某个片的四元组已经在这个集合里,主机会用它的 Offset × 8 算出它该放的起始字节,再根据它携带的数据长度把它"铺"进一个虚拟的缓冲区里,像往拼图里放积木。看下面的过程:

缓冲布局(按片1/片2/片3依次到达):
+--------- 0 -------
|  片2          |     片1到达后填 0~1479,片2 先到也先占着 1480~2959
| 1480~2959     |     最终谁都跑不掉,各自按偏移归位
+----------------...

步骤三:判断"收全了"——等齐才交付

关键的一环来了。重组必须等齐,凑齐所有分片后才能交给上层协议。 它有两条判断依据:

  1. 见尾即知总长:一旦收到某一片的 MF=0,主机会立刻知道"这是最后一片",于是能算出原始数据的完整总长度:总长度 = 最后一片的偏移×8 + 最后一片的数据长度。在我们的例子里就是 370×8 + 20 = 2960 + 20 = 2980 字节,正是原始数据区大小。
  2. 填充无空洞:主机把已到的所有片按偏移铺进缓冲区,检查 [0, 2980) 是否被填满、有没有空洞。一旦填满,就说明"齐了",把拼好的原始数据区连同原始 IP 头(用第一片的信息恢复)一起交给传输层。

绝不可能"缺着一块就把半成品交给上层"——IP 层宁可等,也不会把一个有洞的报文往上递,因为上层协议读到一个烂数据比读不到更糟。

步骤四:超时的兜底——重组计时器

万一有个片在半路丢了、永远到不了呢?主机会傻等吗?不会。IPv4 规范(RFC 791)为重组设了一个重组计时器(通常建议 15 秒):从收到第一个分片算起,如果在时限内没能收齐,主机就把这个四元组对应的所有已收分片全部作废,并丢弃。这样既防止缓冲区被死等占满,也避免了"永远悬置"的资源泄漏。这就是源材料那个 "📌注意" 里要你思考的完整答案。

把接下来的疑问一并打掉:所谓"IP 分片对传输层透明",正是因为这整套"切 + 拼"都发生在 IP 层内部——你发一个 3000 字节的 UDP 报文,分片也好、重组也好,传输层从头到尾看到的只有"我要发的完整报"和"上线收到的完整报",分片的零碎细节被 IP 层包藏尽了。

DF 位与路径 MTU 发现:一场"我不许切"的博弈

分片虽然能解决"过长",却有个天生的隐患——它会让发送端与接收端之间游走的每一个中间片都可能再次被分片,以及"某个片丢了导致整个原始报文报废"的连锁风险。于是网络设计者更推崇一个思路:让发送端在发出之前,就先摸清整条路的最小 MTU,直接把报文控制在每段都无需分片的大小。这就轮到 DF 位和路径 MTU 发现(PMTUD,Path MTU Discovery)登场。

路径 MTU(Path MTU) 指的是从源到目的整条通路上,所有链路的 MTU 里最小的那一个。如果发送的数据报总长度不超过路径 MTU,那它在整条路上就永远不会触发分片。

PMTUD 的做法很巧妙,是一个"先试探、被退回、再收缩"的闭环:

  1. 发送端发出的数据报都设上 DF=1(不许分片);
  2. 如果某条链路发现"这个包超了我的 MTU",因为它被设了 DF,不能切,于是丢弃它,并回送一个 ICMP 报错,报错里带上"我这里的 MTU 是多少";
  3. 发送端收到这个报错,就把自己的发送长度收缩到对方告知的 MTU 之下,再用新的更小尺寸重发;
  4. 一路反复试探,直到整条路都畅通无阻,发送端就收敛到路径 MTU 附近继续通信,全程无需分片。

整个过程可以用一张时序图来看:

发送端                 路由器A(MTU 1500)        路由器B(MTU 1400)        目的端
   |  发 3000 字节,DF=1     |                        |                  |
   |----------------------->|  超 MTU,丢弃并回 ICMP  |                  |
   |<------- ICMP(你的包过大,请求 MTU 1400) ---------|                  |
   |  收缩到 1380,DF=1      |                        |     OK,通路       |
   |----------------------->|----------------------->|----------------->|

这场"不许切"的谈判最经典的一个坑是 MTU 黑洞:如果网络里有人把 ICMP 报错过滤掉了(很多防火墙出于"减少暴露"考虑会这么干),发送端就永远收不到"该收缩"的信号,于是它一遍遍地用超大的、带着 DF 位的包去撞墙,撞一点动静都没有——这就是 MTU 黑洞(Black Hole)。TCP 连接表现为"握手能成、大包死活传不动、卡死"。实践中常用 MSS Clamp 来根治:在网络设备或本机把 TCP 的最大报文段压到某个保守值(比如 1400 以下),从源头就避免产出需要分片的大段,绕开黑洞。这部分在讲 TCP 与分片的关系时会再落一次地。

UDP、TCP、ICMP:谁更容易被分片拖累?

同样是 IP 的载荷,TCP、UDP、ICMP 被分片影响的程度却天差地别。分清这个,对你排障会非常有帮助。

传输层的"透明"不等于"无感",分片只保 IP 层递货

前面说"传输层对分片透明",千万别理解成"传输层永远高枕无忧"。透明的意思是:IP 层会把重组成完整的数据报再掏给上层。可如果这个重组因为丢了一片而失败了,IP 层会把整个原始数据报连同所有已收到的片一起作废——上层拿到的不是"半个",而是"什么都没有"。对上层来说,这件小事 = "我发出去的一个完整报文,凭空没了"。至于它怎么应对,就看是 UDP 还是 TCP 了。

UDP 与分片:丢了就等于丢了

UDP 是无连接的、一层把数据报一次性丢给 IP 就好。也就是说,IP 层能不能装下,UDP 不管:一个 10000 字节的 UDP 数据报,照样会交给 IP 去分片,该切几片切几片。这里藏着 UDP 最脆弱的一面:

  • 分片后,只有一片丢了,整个原始数据报就全部作废,而 UDP 没有重传机制——上层看到的结果就是"这一个 UDP 数据报彻底消失"。
  • 一个长 UDP 报文被切成 N 片,就相当于把原本"一次传输"的风险放大成了"N 片里任一丢失,全盘皆输"。最坏情况下,一个只有 1% 随机丢包率的链路,一个被切成 5 片的 UDP 包,实际到达概率只剩 0.99^5 ≈ 95%,丢一个报文的概率从 1% 涨到接近 5%。越长的 UDP、越多的片,越经不起网络抖动。 这也是为什么语音、NTP 这类典型 UDP 应用,都把报文刻意压得很短。

如果你在抓包时看到一个大 UDP 包被打散成好几片,就要警惕:这往往说明应用把 UDP 报文发得偏大、而路径上的 MTU 又偏小,未必是什么好设计。

TCP 与分片:靠 MSS 主动躲开,靠重传兜底

TCP 是"流式 + 面向字节 + 可靠"的协议,它对待"分片"和 UDP 完全不同,拿它是一种主动防御:

  • 最重要的武器是 MSS(Maximum Segment Size,最大报文段大小):TCP 在建立连接的三次握手时,双方会把自己能接受的最大报文段协商出来,一般取"本机出接口 MTU - 20 字节 IP 头 - 20 字节 TCP 头",以太网下通常是 1460。于是 TCP 每次就只发这么大的段,天然避免了"一个段太大导致 IP 分片"的情况。
  • 即便分片万一发生了(例如穿越了 MTU 更小的隧道、或协商阶段双方各自认为的口径不同),TCP 依然有完整可靠保证:若某一片丢失导致原始段作废,TCP 会由于"没收到确认"而重传整个段,重传时再被分片又再来一遍,直到成功。所以对 TCP 来说,分片丢片只是"让我重传一次",属于容错范围内的插曲,不会像 UDP 那样直接丢失数据。

一句话总结分工:TCP 用 MSS 尽量不产生分片、又用重传兜住万一发生的分片丢片;UDP 既不用 MSS 也重传不了,一分片就裸奔。

ICMP 与分片:报错包自己也可能被切

你可能觉得报错类的东西很轻、很短,但别忘了一条硬规矩:ICMP 报文也是 IP 的载荷,同样要受 MTU 约束、同样会被分片。 比如 ping -s 4000 发了一个大负载的 ICMP Echo 请求,它照样会被切成好几片。把这三种典型载荷在分片问题上的站位收进一张表:

上层协议有没有 MSS/长度抑制分片丢一片的直接后果
TCP有,主动控制段长无确认触发重传,可靠兜底
UDP无,数据报一次性丢给 IP整个数据报作废,直接丢数据
ICMP无(报文基本短小为主)主要影响大负载 ping 等探测,丢片=探测无响应

分片世界里的边界与坑

我们把最容易踩的坑集中摆出来,每一个都值得背下来:

  1. 片偏移的单位必须是 8 字节。这是前 13 位字段的数学宿命。写错成一字节单位,重组的字节序就会整体错位,拼出来的是一坨垃圾。
  2. 每片长度必须是 8 的倍数,除了最后一片。因为下一片的偏移要靠"上一片长度是 8 的倍数"来保证连续性;只有尾部不受此限。
  3. 重组必须等齐。IP 层在重组合集填满、无空洞之前,绝不会把半成品交给上层,到点收不齐就整个作废。
  4. DF 位 + 大包 + ICMP 被过滤 = MTU 黑洞。快去简单记住这条:设了 DF 的大包在过不去时会双亡——既丢包又没反馈,特征就是"大包断、小包通"。
  5. 分片有额外开销。每片都要多背 20 字节 IP 头,片越多浪费越大;而且分片只在目的主机重组,中间路由再分一次就更多浪费。
  6. 分片增加丢包风险(尤其 UDP):一个原始报文被切成 N 片,N 片齐全才算成功,任一丢失全盘皆废。
  7. 重组在目的主机完成:IPv4 下路由器只管切、不负责拼,只有目的主机按(源IP、目的IP、协议、标识)收齐重组成全长。

把这些再浓缩成一句顺口的心法,方便随身携带:"偏移对齐靠 8、重组等齐再交付、DF 位是句硬话"。

自测与思考题(每题详解)

下面四道题覆盖了全篇的基础与边界,请先自己写,再对照详解。

(1) 为什么片偏移只用 13 位的字段,却能把一个最大 65535 字节的报文的位置表示完整?

详解:13 位二进制只能表示 0 到 8191 这 8192 个值。如果按 1 字节为单位,最多描述到第 8191 字节的偏移,远不足以覆盖 IPv4 里 16 位"总长度"最大可达的 65535 字节。标准的做法是把单位放大到 8 字节:这样字段能表达的最大字节偏移为 8191 × 8 = 65528 字节,加上"原始数据报总长由最后一篇的偏移和长度共同算得",就能覆盖到完整的 65535 字节范围。也就是说,这是用"精度换范围"——用单位放大8倍,换取更宽的描述能力,代价是偏移必须是 8 的倍数、中间片长度必须是 8 的倍数。

(2) 一个 IP 数据报总长 4000 字节(IP 头 20 字节),MTU = 1500,请给出每一片的:偏移字段、数据长度、总长度、MF 的值,并验证。

详解:

数据区长度 = 4000 - 20 = 3980 字节
每片最大载荷 = 1500 - 20 = 1480(8 的倍数,可用)
3980 = 1480 + 1480 + 1020
  • 片1:数据 [0,1479],长度 1480,偏移字段 0/8 = 0,总长 20+1480=1500,MF=1;
  • 片2:数据 [1480,2959],长度 1480,偏移字段 1480/8 = 185,总长 1500,MF=1;
  • 片3:数据 [2960,3979],长度 1020,偏移字段 2960/8 = 370,总长 20+1020=1040,MF=0。

验证:1020 不是 8 的倍数(1020÷8=127.5),但它是最后一片,合法;前两片 1480 是 8 的倍数,保证偏移连续。数据总数 = 1480+1480+1020 = 3980,正确,完整覆盖 [0, 3979],与原始数据区 3980 字节完全一致,无误。

(3) 假设上面的第 2 个分片在网络上丢失,目的主机会发生什么?对 UDP 和 TCP 各意味着什么?

详解:目的主机在重组缓冲区里看到了片1(MF=1)和片3(MF=0),但 [1480, 2959] 这一段始终是空洞,凑不齐,因此永远无法满足"填满无空洞"的条件;等重组计时器(约 15 秒)超时,IP 层把这三个片全部作废并丢弃。对 UDP:接收端永远等不到这个数据报,直接丢了这一次应用数据(没有重传机制)。对 TCP:因为 TCP 收不到一个完整的段,就等不来该段的确认,超时后重传整个段,重新分片再来一遍,一般最终能恢复——但它会体现出一次"额外的延迟和重传"。这正道出了"分片丢一片,全盘皆废,只有可靠传输层能兜底"的道理。

(4) 什么是路径 MTU?为什么要做 PMTUD?如果路径上有人把所有 ICMP 报错都过滤了,会发生什么?

详解:路径 MTU 是源到目的整条通路上所有链路 MTU 的最小值,只要发送数据报不超过它,一路上就不会触发分片。PMTUD 通过"设 DF 发出 → 遇阻回 ICMP(含目标 MTU) → 收缩重发"的循环,让发送端在真正大流量之前就探明路径 MTU,从而避免分片、以及分片带来的各种浪费和丢包风险。如果 ICMP 报错被过滤,发送端永远收不到"请收缩"的信号,会不断用 DF=1 的超大包去撞;这些包统统被丢弃,也不会有任何反馈——这就是 MTU 黑洞:表现为"大包不通、小包能通"、TCP 连接握手成功却传不动大数据。缓解手段通常是做 MSS clamp,把 TCP 数据段的长度在路径入口就压小,从源头避免产出可能超 MTU 的大段。


走到这里,我们已经把 IP 分片从"为什么切"一路拆到"怎么切、怎么拼、怎么防",再落到各大协议头上。核心就三件念念不忘的事:偏移字段是 13 位的、单位 8 字节;中间片长度是 8 的倍数、最后一片例外;重组在目的主机、要到齐才交付、DF 配上黑洞是经典故障。这三句话能串起你几乎所有关于分片的后续排障。

下次当你在抓包里看到 [IP fragment offset] 那一列、或打开一个被切成好几段的 UDP 时,希望你能一眼看穿:这不是什么玄学,只是一张"13 位不够用、于是按 8 字节记账"的协议算术,外加一段"总能等齐再交"的重组承诺。网络世界里的很多"奇观",说到底都是这种朴素的权衡。