【Linux 网络编程】IP 分片与重组:3000 字节的数据报,怎样拆开又拼回来?

🔥 星光编译者 · 个人主页

📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》

🌄 向云端飞扬,编译属于自己的代码星河。


☕ 写在开篇

  你好,这里是 星光编译者。

  这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。

  比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。

  如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。


🔥 本文定位:在认识 IPv4 首部之后,沿着一份数据报走完"超过 MTU → 分片 → 独立传输 → 乱序到达 → 重组"的全过程。

💡 学习目标:算对每片长度、MF 与片偏移;判断哪些分片属于同一数据报;解释接收端如何确认收齐;用离线实验和 Linux 抓包读法验证理解。

📌 阅读范围:以普通 IPv4 单播为主。计算示例统一采用无选项的 20 字节 IPv4 首部;分片示例默认 DF=0,且没有 NAT、隧道和过滤干预。IPv6 的差异单独说明。

假设 IP 层准备发送一份总长 3000 字节的数据报,出口接口的 MTU 却只有 1500 字节。

"分成两份,每份 1500 字节"看起来很合理,但只要继续问一句------拆开以后,每一份自己的 IP 首部放在哪里?------这个答案就站不住了。

接收端面临的问题更多:先收到最后一片怎么办?两台主机恰好用了相同的 ID,能不能混在一起?已经收到三片,是否就说明收全?如果路径上还有一个更小的 MTU,已经分过的片还能再拆吗?

这些问题并不需要靠死记首部图来解决。把"长度""位置"和"完整性"分开,再用同一组数字算一遍,IP 分片就会变得很具体。


文章目录

  1. [为什么需要分片:先把 MTU 与层次说清楚](#为什么需要分片:先把 MTU 与层次说清楚)
  2. [ID、DF、MF 和片偏移,各自回答什么](#ID、DF、MF 和片偏移,各自回答什么)
  3. [完整算一次:3000 字节如何分成三片](#完整算一次:3000 字节如何分成三片)
  4. 每片独立传输,哪些首部会重复
  5. 接收端怎样组装:归组、定位与判断收齐
  6. 已经分片,还能再次分片吗
  7. 丢一片为什么影响整份数据,以及怎样减少分片
  8. 离线实验:模拟乱序、缺片与二次分片
  9. [Linux 抓包:把线上字段读回计算过程](#Linux 抓包:把线上字段读回计算过程)
  10. 常见误区与三道自测题

一、为什么需要分片:先把 MTU 与层次说清楚

1.1 MTU 限制的是当前链路承载的 IP 报文大小

MTU 是 Maximum Transmission Unit,最大传输单元。在本文的 IPv4 over Ethernet 场景中,接口 MTU=1500 表示这条链路通常允许承载的 IP 报文总长度最大为 1500 字节,其中已经包含 IP 首部。

不要把 1500 当成应用正文长度,也不要再从中扣掉以太网首部和 FCS。以太网的这些字段位于外层,不包含在这个 IP MTU 数字里。

以无选项的 IPv4 + UDP 为例:

text 复制代码
1500 字节 IP 报文
= 20 字节 IPv4 首部
+ 8 字节 UDP 首部
+ 1472 字节应用数据

这个等式有两个前提:本例采用 MTU 1500,且 IPv4 首部没有选项。换成不同的 MTU、额外封装或不同首部长度,就必须重新计算。

1.2 链路给出限制,IPv4 层执行分片

分片的直接原因是当前出口无法一次容纳这份 IP 报文,但执行这里所说的 IPv4 分片的是 IP 层。它切分 IP 载荷,为每一片设置自己的 IP 首部,再交给下层封装发送。

因此,"因为链路 MTU 不够而分片"与"由数据链路层执行 IP 分片"不是同一回事。链路协议也可能有自己的分段机制,不能与 IPv4 的 MF、Fragment Offset 混为一谈。

分片既可能发生在源主机,也可能发生在转发途中。比如源主机的出口可以承载 3000 字节,而某台路由器的下一段链路只允许 1500 字节,那么在允许分片的条件下,这台路由器就可能拆分数据报。

1.3 大于 MTU,并不保证一定能分片发出去

判断要同时看大小和 DF:

当前条件 基础处理思路
IP 总长不超过出口 MTU 无需因为这一跳的 MTU 而分片
IP 总长超过出口 MTU,DF=0 允许 IPv4 分片;实际发送还受系统与网络策略影响
IP 总长超过出口 MTU,DF=1 禁止分片,不能直接把超长报文塞进该链路

源主机可能在发送时就返回"消息过长"一类错误;转发路由器遇到不能分片的超长报文,则会丢弃,并在符合 ICMP 差错发送条件时返回需要分片的通知。不要把本地错误和途中 ICMP 混成一个固定现象。


二、ID、DF、MF 和片偏移,各自回答什么

2.1 Identification:这是哪一份原始数据报的片段

IPv4 的 Identification 字段长 16 位,通常简称 ID。同一份原始数据报拆出来的各个分片,携带相同的 ID。

但接收端不能只按照 ID 归组。基础协议层面的重组标识是:

text 复制代码
源 IP + 目的 IP + 上层协议号 Protocol + Identification

例如,来自不同源 IP 的两个 UDP 数据报,即使都使用 ID=4242,也不能拼成同一份数据。Protocol 则区分 TCP、UDP 等上层协议;它不是端口号。

ID 只有 16 位,也不能理解成全互联网永不重复的编号。有关唯一性的要求有相应范围和生存期条件;对不会被分片的原子数据报,更新后的规范也不要求把 ID 当作通用递增序列。排查时不要用 ID 的连续性判断应用是否丢消息。RFC 6864:IPv4 ID 的用途与约束

2.2 DF 和 MF:一个管"能不能拆",一个管"后面还有没有"

Flags 共 3 位,依次是保留位、DF、MF。保留位应为 0。

字段 值为 0 值为 1
DF,Don't Fragment 允许分片 禁止分片
MF,More Fragments 本片后没有更多原始载荷 本片后还有更多原始载荷

DF=0 只说明允许拆,报文很小时完全可以不拆。MF=0 也不等于"这个报文从未分片",因为一组分片的最后一片同样是 MF=0。

把 MF 与片偏移放在一起,才好判断:

MF Fragment Offset 对有效报文的常见解释
0 0 未分片报文,或重组后恢复的形式
1 0 首片
1 大于 0 中间片
0 大于 0 末片

所以,检查"这是不是分片"的基础条件是:MF=1,或者 Fragment Offset 不为 0。

2.3 Fragment Offset:从原始 IP 载荷的哪里开始

片偏移字段长 13 位,单位是 8 字节 ,基准是原始 IP 数据报的数据区起点,不包含原始 IP 首部。

两个方向的公式要分清:

text 复制代码
字节起点 = Fragment Offset 字段值 × 8
Fragment Offset 字段值 = 字节起点 ÷ 8

字段值 185,表示从原始 IP 载荷的第 1480 字节处开始,不是从第 185 字节开始,更不是再除以 8。

这里按从 0 开始的字节下标计数。为了避免"最后一个字节算不算在内"的歧义,下文使用左闭右开区间:[0, 1480) 表示包含下标 0~1479,共 1480 字节。

2.4 为什么非末片载荷长度要是 8 的倍数

下一片的起点必须能够用 8 字节单位表达。如果前一片携带 1479 字节,下一片应该从字节 1479 开始,但 1479 ÷ 8 不是整数,偏移字段就无法精确表示。

因此,合法分片中 MF=1 的片,其载荷长度必须按 8 字节对齐。末片没有后继起点要表达,载荷长度可以不是 8 的倍数。

这里有两个容易写反的细节:

  • 要对齐的是分片载荷,不是"IP 首部 + 载荷"的总长度。
  • 末片的起点仍然按 8 字节对齐,只是末片的长度不一定对齐。

三、完整算一次:3000 字节如何分成三片

3.1 先消除"3000 字节"本身的歧义

本节明确约定:

text 复制代码
原始 IP 总长:3000 字节,包含 IP 首部
IPv4 首部:20 字节,无选项
原始 IP 载荷:3000 - 20 = 2980 字节
出口 MTU:1500 字节
DF:0
ID:4242

如果题目说的是"应用数据 3000 字节"或"IP 载荷 3000 字节",答案都会不同。因此算分片之前,第一句应该是:这个长度包含哪些首部?

3.2 算出每个非末片最多放多少载荷

扣掉本片自己的 IP 首部,最多还剩:

text 复制代码
1500 - 20 = 1480 字节

1480 正好是 8 的倍数,所以可以直接作为非末片载荷长度。

一般地,在本文无选项、固定首部长度 H 的模型中:

text 复制代码
非末片最大载荷 = floor((MTU - H) / 8) × 8

例如 MTU=1499 时,虽然扣掉首部剩 1479 字节,但非末片只能取 floor(1479 / 8) × 8 = 1472 字节。剩余空间不能靠破坏对齐来填满。

注意:若整份报文本来就能放进 MTU,不需要为了套这个公式强行切开。只有需要拆分时,才讨论非末片的对齐上限。

3.3 按原始载荷顺序切三段

2980 字节载荷切分为:

text 复制代码
第一片 A:[0, 1480)       → 1480 字节
第二片 B:[1480, 2960)    → 1480 字节
第三片 C:[2960, 2980)    →   20 字节

再分别补上 20 字节 IP 首部:

分片 原始载荷区间 本片载荷 本片 IP 总长 Offset 字段值 MF ID
A [0, 1480) 1480 1500 0 1 4242
B [1480, 2960) 1480 1500 185 1 4242
C [2960, 2980) 20 40 370 0 4242

B 的偏移为 1480 / 8 = 185;C 的偏移为 (1480 + 1480) / 8 = 370。片偏移始终从最初那份 IP 载荷的起点算,不是相对前一片计算。

3.4 用三个检查证明结果自洽

第一,载荷守恒:

text 复制代码
1480 + 1480 + 20 = 2980

第二,每片都能通过当前链路:1500、1500、40 均不超过 MTU 1500。

第三,区间连续:A 的结束位置等于 B 的开始位置,B 的结束位置等于 C 的开始位置,没有空洞,也没有重复覆盖。

所有分片的 IP 总长之和则是:

text 复制代码
1500 + 1500 + 40 = 3040 字节

比原始 3000 字节多出的 40 字节,来自新增的两份 IP 首部。这不是多发了应用数据。这里也没有计入每个以太网帧的额外开销。

3.5 如果"3000"指 IP 载荷呢

此时原始 IP 总长变为 3020 字节,载荷切分为 1480 + 1480 + 40,三个分片的 IP 总长为 1500、1500、60,Offset 仍是 0、185、370。

如果"3000"指 UDP 应用数据,那么还要加上 8 字节 UDP 首部:IP 载荷为 3008 字节,最后一片载荷变为 48 字节,总长为 68 字节。

先确定层次,再扣首部,再做对齐,最后填字段。 这个顺序比记住某组数字更可靠。


四、每片独立传输,哪些首部会重复

4.1 每片都有 IP 首部,不代表每片都有 UDP 首部

把原始数据报写成:

text 复制代码
IPv4 首部 | UDP 首部 | 应用数据

从 IP 的角度看,UDP 首部也是载荷的一部分。IP 分片切的是 UDP 首部 + 应用数据 这一整段字节,不会给每一片重新构造一个完整 UDP 数据报。

沿用刚才的 2980 字节 IP 载荷,假设上层协议为 UDP:

text 复制代码
原始 IP 载荷 = 8 字节 UDP 首部 + 2972 字节应用数据

A:20 字节 IP 首部 + 8 字节 UDP 首部 + 1472 字节应用数据
B:20 字节 IP 首部 + 1480 字节后续应用数据
C:20 字节 IP 首部 +   20 字节后续应用数据

本例首片空间充足,完整包含 UDP 首部。对于一般传输层协议,不应把"任意首片都一定包含完整传输层首部"当作无条件规律;分片大小与首部长度也要结合来看。

4.2 相同与变化的字段,要分别看

在本文的普通路由、无选项示例中:

项目 分片后怎样理解
源 IP、目的 IP、Protocol、ID 同组分片保持对应关系,用于归组与上层分用
Total Length 描述本片总长,不再填写原始的 3000
MF、Fragment Offset 随片的位置而变化
Header Checksum 按各片自己的首部重新计算;只校验 IP 首部
TTL 随转发处理递减;不同路径可能导致到达时不同
链路层封装 每片分别封装,跨链路转发时按下一跳重新构造

如果存在 IPv4 选项,哪些选项复制到后续片还有专门规则,首部长度也可能变化。因此本文固定 20 字节首部的算式不能不加修改地套到所有带选项报文上。RFC 791:分片时的字段与选项处理

4.3 独立传输,不等于一定走不同路线

每片作为独立 IP 报文被转发,所以接收端不能依赖它们按原顺序到达。但"独立"不等于每片必然选择不同路径:在稳定的路由条件下,它们也可能沿同一路径到达。

更关键的是普通转发过程的职责:路由器不需要先收齐原始数据报的所有片,再进行下一跳转发。最终 IP 接收端负责重组。连接跟踪、隧道端点或其他中间处理机制可能引入重组行为,那属于额外场景,不能反过来当成普通路由的必经步骤。


五、接收端怎样组装:归组、定位与判断收齐

5.1 第一步:找到同一份数据报的重组上下文

接收端首先检查报文的基本合法性,再用源地址、目的地址、Protocol、ID 等信息,找到对应的重组上下文。实际内核还可能把网络命名空间等本地上下文纳入管理。

可以把这个上下文理解成一张"收件登记表":它保存已经收到哪些字节区间、是否知道最终长度、还缺哪些部分,以及等待期限。

这不是应用层的 recv 缓冲区。应用一般不会收到 A、B、C 三份独立的 UDP 消息,再自己把它们拼起来;重组发生在交给普通传输层处理之前。

5.2 第二步:根据 Offset 放到原来的位置

对每一个有效片,先计算:

text 复制代码
本片首部长度 H = IHL × 4
本片载荷长度 L = Total Length - H
本片开始位置 start = Fragment Offset × 8
本片结束位置 end = start + L

因此 C 虽然只有 20 字节载荷,却应该放在 [2960, 2980),而不是放在已经收到的数据后面。

"按 Offset 排序再拼接"有助于入门,但实现并不一定真的先维护一个排好序的数组。它可以使用缓冲区、区间结构或空洞记录;关键是最终字节的位置与完整性正确。

5.3 第三步:末片告诉你终点,连续覆盖告诉你收齐

若收到 MF=0 的末片,就能推导原始 IP 载荷的总长度:

text 复制代码
原始载荷总长 = 末片 Offset × 8 + 末片载荷长度
             = 370 × 8 + 20
             = 2980 字节

但是,知道终点在哪里,并不代表起点到终点之间已经全部填满。

假设到达顺序是 C → A → B:

到达时刻 已收到的区间 已知的缺口 是否可以交付
只收到 C [2960, 2980) [0, 2960) 不可以,虽然已经知道总长
再收到 A [0, 1480)、[2960, 2980) [1480, 2960) 不可以,中间仍缺一整片
最后收到 B [0, 2980) 连续覆盖 无 通过其余校验后可以重组交付

对本例的合法、无重叠分片,可以把完成条件概括为:已知最终载荷长度,且从 0 到这个长度的字节区间完整连续。

IP 首部没有一个字段直接写着"总共三片"。所以不能说"收到了三片就完成",也不能因为三个片的长度之和恰好相等就直接交付。重复片会让简单累加得到错误结论。

5.4 缺片、重复片和重叠片怎么处理

缺片会留下空洞。系统不会永远等下去:重组有超时和资源限制,最终可能丢弃无法完成的上下文。具体超时时间属于实现与配置,不能写成所有系统统一的固定秒数。

重复片描述已经覆盖过的范围,不会让有效数据凭空增加。重叠片更麻烦:不同分片可能声称自己覆盖同一段字节,甚至给出不同内容。对这些异常,具体系统的检查和丢弃策略可能不同,不能用"后到的数据覆盖先到的数据"概括所有实现。

学习时可以采用严格的无重叠模型,先把正常分片和空洞判断算明白。RFC 815 讨论的空洞思想,就提供了从"还缺哪里"理解重组的角度;它不是要求所有操作系统使用同一份数据结构。RFC 815:IP 重组中的空洞管理思路


六、已经分片,还能再次分片吗

6.1 下一跳 MTU 更小,原来的片也可能放不下

假设第二片 B 的总长为 1500,继续前进时遇到 MTU=620 的出口。DF=0,首部仍为 20 字节,则非末片的最大载荷为:

text 复制代码
floor((620 - 20) / 8) × 8 = 600 字节

B 的 1480 字节载荷于是继续拆成 600 + 600 + 280。

6.2 二次分片的偏移不能重新从 0 开始

B 在原始载荷中的起点本来就是 1480。三个新片的绝对起点因此分别是:

text 复制代码
B1:1480          → Offset = 185
B2:1480 + 600    → Offset = 260
B3:1480 + 1200   → Offset = 335

这里可以直接使用字段单位计算:

text 复制代码
新 Offset = 旧 Offset + 在旧片载荷内的字节偏移 / 8

6.3 B3 是这次拆分的最后一片,为什么 MF 仍是 1

因为 MF 问的是:本片之后,原始数据报是否还有载荷?

B 原本就不是整份数据报的末片,后面还有 C 的 20 字节。于是 B1、B2、B3 都应该是 MF=1。不能因为 B3 是"B 拆出来的最后一个",就把它设为 MF=0。

新片 原始载荷区间 载荷 IP 总长 Offset MF
B1 [1480, 2080) 600 620 185 1
B2 [2080, 2680) 600 620 260 1
B3 [2680, 2960) 280 300 335 1

概括二次分片的 MF 规则:前面的新片置 1,最后一个新片继承旧片的 MF。只有原始数据报的真正末尾,才可能得到 MF=0。

这里为了聚焦偏移,只展开了 B。若 A 同样经过这个 MTU=620 的出口,它也需要进一步拆分;C 总长 40,可以直接通过。目的端仍使用原始载荷的统一坐标重组,无需先恢复 B,再恢复整份数据。


七、丢一片为什么影响整份数据,以及怎样减少分片

7.1 IP 不负责替缺失的片安排重传

如果 B 丢失,接收端即使收到了 A 和 C,也不能把不完整的原始 IP 载荷作为正常数据交给上层。等待超时后,这次重组可能失败。

IP 本身没有"请把 ID=4242 的第二片再发一次"的可靠重传协议。若上层是 TCP,可靠性由 TCP 根据确认、丢失检测等机制恢复相应字节;UDP 本身不自动重传,应用是否重试由自己的协议决定。

因此,"IP 分片对传输层透明"主要描述接口与处理层次:普通传输层收到的是重组后的内容。它不意味着分片丢失对延迟、吞吐量和应用成功率没有影响。

7.2 分片越多,成功交付需要同时满足的条件越多

做一个只用于直觉的概率模型:假设各片独立丢失,且每片丢失概率同为 p,那么 n 片全部到达的概率为:

text 复制代码
P(全部到达) = (1 - p)^n

若 p=1%,单片成功率为 99%,三片全部成功约为 97.03%,十片全部成功约为 90.44%。真实网络的丢包可能相关,且包长也会影响行为,不能拿这个简化模型直接预测生产网络。

除此以外,分片还增加首部开销与重组状态。更小的报文也可能带来更多包处理开销,所以实际优化应结合业务和路径测量;这里只说明为什么不宜把大量 IP 分片当作默认设计目标。

7.3 PMTU 与 TCP MSS 分别做什么

接口 MTU 描述某一条链路,Path MTU 则关注整条路径能承载的大小。经典 IPv4 PMTU 发现利用 DF 和 ICMP "需要分片"反馈,帮助发送端调整报文大小。如果必要的反馈收不到,可能出现小包可达、大包失败等现象,但这种现象本身不能直接证明根因。RFC 1191:Path MTU Discovery

TCP 则会在传输层切分字节流。MSS 描述 TCP 数据部分的大小上限,不包含 TCP 与 IP 首部;实际发送段还受路径、选项、窗口等约束影响。

在无 IP/TCP 选项的 MTU 1500 示例里,经常看到:

text 复制代码
IP 载荷上限:1500 - 20 = 1480
TCP 数据上限:1500 - 20 - 20 = 1460

所以不能把 1480 直接称为"TCP 应用数据最大长度"。TCP 分段与 IP 分片也不能混为一谈:前者形成各自带 TCP 首部的段,后者拆分一份 IP 载荷。

7.4 IPv6 不照搬 IPv4 路由器分片

IPv6 中,转发路由器不执行这种途中分片;需要分片时由源节点处理,并使用 Fragment 扩展首部。遇到不能转发的超大包,路由器通过 ICMPv6 Packet Too Big 等机制反馈。

这也是为什么本文反复限定"IPv4":IPv4 的 16 位 ID、DF、MF 首部布局和路由器分片方式,不能直接套到 IPv6 上。RFC 8200:IPv6 分片与重组


八、离线实验:模拟乱序、缺片与二次分片

8.1 先在本地把数字算明白

文章包的 code/ipv4_fragment_lab.py 使用 Python 标准库,不发送网络报文,也不修改系统配置。从文章目录运行:

bash 复制代码
python3 code/ipv4_fragment_lab.py calculate --total 3000 --mtu 1500
python3 code/ipv4_fragment_lab.py demo
python3 code/ipv4_fragment_lab.py selftest

Windows 环境可根据安装方式将 python3 换成 python。这是离线教学模型,限定无选项 IPv4 首部、单一数据报的重组上下文;它不模拟 Linux 的真实队列、超时、路由或 PMTU 策略。

calculate 的预期关键输出为:

text 复制代码
part  start   end   payload  total  offset  MF
   1      0  1480      1480   1500       0   1
   2   1480  2960      1480   1500     185   1
   3   2960  2980        20     40     370   0

8.2 真正值得观察的是"缺口"

demo 把载荷构造成可比对的字节序列,按 C → A → B 送入重组器,展示末片先到时的缺口变化。只有第三次输入后,返回的数据才应与原始 2980 字节完全一致。

再移除 B,只输入 A 与 C,结果应保持不完整。最后把所有初始分片继续经过 MTU=620 的拆分器,会得到以下 Offset:

text 复制代码
0、75、150、185、260、335、370

前六片 MF=1,最后一片 MF=0;它们仍应可以乱序重组回相同载荷。这比只比较总字节数更有说服力,因为错误的片序也可能保留相同长度。

8.3 核心计算其实很短

下面是正常分片场景中的核心思路,完整参数校验与重组演示见配套脚本:

python 复制代码
header_len = 20
capacity = ((mtu - header_len) // 8) * 8
start = 0

while start < len(payload):
    # 末片可直接容纳剩余字节,不要求长度是 8 的倍数。
    remaining = len(payload) - start
    size = remaining if remaining <= mtu - header_len else capacity
    end = start + size
    offset_field = start // 8
    mf = int(end < len(payload))
    fragment_payload = payload[start:end]
    start = end

这一小段没有构造真实 IP 首部,也没有发送数据。它只把"从哪里切、切多少、字段填什么"的关系展开。实际二次分片还必须加上旧片的起点,并让最后一个新片继承旧 MF。

配套重组器采用保守策略:拒绝重叠片及矛盾的末片信息,避免把教学代码误当作跨平台的真实内核行为。它也没有定时器,因此缺片示例只展示"不完整",不会假装演示系统超时。


九、Linux 抓包:把线上字段读回计算过程

9.1 先确定接口与目标,再制造少量测试流量

下面使用 Linux 的 iputils ping 和 tcpdump。将接口名及 192.0.2.2 替换成你自己的测试环境;这个地址只是文档示例,不是可直接探测的公共服务。

bash 复制代码
# 查看接口概况,并查询去往测试目标的出口
ip -brief link
ip -4 route get 192.0.2.2

# 将 eth0 换成上面查询到的出口;完整输出中可看到 mtu 字段
ip link show dev eth0

# 终端 A:观察指定目标的 IPv4 流量
sudo tcpdump -i eth0 -nn -vv -s 0 -c 30 'ip and host 192.0.2.2'

# 终端 B:发送两次允许 IPv4 分片的 ICMP Echo 请求
ping -4 -c 2 -W 2 -M dont -s 2972 192.0.2.2

为什么是 -s 2972?ping 的这个参数是 ICMP 数据长度。在无 IP 选项的 Echo 请求中:

text 复制代码
20 字节 IPv4 首部 + 8 字节 ICMP 首部 + 2972 字节数据 = 3000 字节

-M dont 表示不设置 DF。如果实际出口 MTU 为 1500,并且路径与策略符合本文条件,便可观察与计算例子对应的分片。不要使用环回接口期待相同结果:环回 MTU 通常更大,不一定会触发这个例子中的切分。ping(8):-M 和 -s 参数

抓包达到 30 包才会自动退出;测试结束仍不足这个数量时,按 Ctrl+C 停止。请求和响应可能都有分片,比较时按源/目的方向区分它们各自的分片组。

这里给出的是可复现步骤与预期字段,不是某台 Linux 主机的实测截图。实际 MTU、工具版本、过滤规则与抓包位置都可能影响观察。

9.2 不要让端口过滤漏掉后续片

对 UDP 分片使用 udp port 9000 过滤时,非首片没有位于片首的 UDP 首部,端口条件可能把它们排除。于是你以为"只发了第一片",实际上只是过滤条件看不到后续片。

观察一组分片时,可以先用 IP 地址缩小范围,再使用 IPv4 字段条件:

bash 复制代码
# MF=1 或片偏移非零:覆盖首片、中间片与末片
sudo tcpdump -i eth0 -nn -vv -s 0 \
  'ip and host 192.0.2.2 and (ip[6:2] & 0x3fff != 0)'

# 只看片偏移非零的后续片;这条有意不包含首片
sudo tcpdump -i eth0 -nn -vv -s 0 \
  'ip and host 192.0.2.2 and (ip[6:2] & 0x1fff != 0)'

ip[6:2] 读取 IP 首部中 Flags 与 Fragment Offset 组成的 16 位字段。掩码 0x1fff 取偏移部分;0x3fff 额外保留 MF,排除 DF 和保留位。因此未分片但 DF=1 的普通报文不会被第一个表达式误选成分片。pcap-filter(7):字段访问与分片过滤

9.3 抓包工具显示的 offset,可能已经换成字节

本文表格里的 Offset=185、370,是报文首部字段的原始单位。tcpdump 的常见文本输出会显示换算后的字节偏移,例如 1480、2960。

因此看到下面这组信息时,要先确认显示单位:

text 复制代码
A:id 4242,offset 0,   MF=1,IP length 1500
B:id 4242,offset 1480,MF=1,IP length 1500
C:id 4242,offset 2960,MF=0,IP length 40

这是一份按字节解释的字段示意 ,不是逐字复制的终端输出。实际工具可能用 flags [+] 表达 MF,也可能在详细字段面板里同时给出原始值与换算值。不要把工具已经换算过的 1480 再乘以 8。

另一个常见混淆是 length:IP 总长、UDP 长度、应用载荷长度和捕获帧长度并非同一层次。看到数字时,先看它对应的协议和显示位置。

9.4 改成 DF=1,观察另一种处理路径

可以再做两次很小规模的对照:

bash 复制代码
ping -4 -c 2 -W 2 -M do -s 2972 192.0.2.2

-M do 设置 DF,并受内核 PMTU 检查影响。如果本地已知道尺寸超限,可能直接报本地错误,根本不会发出这个大包;也可能在途中触发相应 ICMP 差错。没有看到分片,不等于 tcpdump 一定坏了。

观察结果还应结合抓包位置。发送端看到的"大包"可能受 GSO/TSO 等卸载影响;接收侧合并也可能改变可见形态。优先结合接收端或链路上的抓包,不要把本机捕获到的一条记录直接等同于线上一个以太网帧。


十、常见误区与三道自测题

10.1 这些结论,补上条件才准确

常见说法 更准确的理解
IP 报文超过 MTU 就一定分片 还要看 DF、发送策略与实际处理条件
分片由数据链路层完成 本文讨论的 IPv4 分片由 IP 层执行,链路 MTU 提供约束
ID 相同就属于同一个数据报 还需结合源 IP、目的 IP、Protocol 等上下文
Offset=185 表示偏移 185 字节 原始字段单位为 8 字节,对应 1480 字节
偏移从原始 IP 首部开始算 从原始 IP 载荷的起点开始算
每个分片总长都必须是 8 的倍数 MF=1 分片的载荷长度按 8 字节对齐
MF=0 表示没有分片 也可能是偏移非零的末片
末片到了就可以交给 UDP 还要确保整个载荷区间完整覆盖
IP 首部中保存了分片总数量 没有这个总片数字段,收齐要根据区间判断
每一片都会复制 TCP/UDP 首部 传输层首部是原始 IP 载荷的一部分,不逐片重建
二次分片从 0 重新编号 继续使用原始载荷坐标,末个子片继承旧 MF
IP 会负责重传丢失的第二片 IP 不提供这种可靠重传机制
IPv6 路由器与 IPv4 一样拆包 IPv6 转发路由器不执行这种途中分片

10.2 自测一:MTU 变成 1499,怎么算

仍然是总长 3000、IP 首部 20、DF=0。

非末片最大载荷为 1472,因而载荷切分为 1472、1472、36;分片总长为 1492、1492、56;Offset 为 0、184、368;MF 为 1、1、0。

前两片没有把 1499 字节全部用满,是因为要满足 8 字节对齐。末片 36 字节不是 8 的倍数,不影响它作为末片存在。

10.3 自测二:只有 MF=0、Offset=370,够不够判断收齐

不够。首先还要知道这片实际有多少载荷,才能推导总长度;其次即使已经知道总长度,也需要核对前面的区间是否连续覆盖。

如果该片 IP 总长为 40、IHL=5,那么载荷为 20,总长度可算为 2980。但它完全可能是最先到达的片,此时 [0, 2960) 仍然全都缺失。

10.4 自测三:B3 的 MF 错设成 0 会怎样

B3 的 Offset=335,载荷 280。如果把 MF 错写成 0,就声称原始载荷在 335 × 8 + 280 = 2960 处结束。

真正末片 C 表明终点是 2980。如果 C 已经进入同一个重组上下文,再收到这个错误的 B3,就会发现两个终点矛盾;配套教学模型会拒绝这种输入。

但到达顺序很重要:如果错误的 B3 先到,且 [0, 2960) 已经连续覆盖,IP 层仅凭当前分片信息无法预知后面还应有 C,可能把这个较短的结果视为重组完成。配套模型也会返回这 2960 字节。因此不能声称"MF 写错一定会被 IP 重组器提前发现"。

后续还需要上层检查。沿用本文的 UDP 示例,若 UDP 首部保持不变,其中的 Length 仍声明 2980 字节,与这个较短结果不符;UDP 的长度检查可以发现这种截断。UDP Length 包括 UDP 首部和数据,这与 IP 分片首部的 Total Length 是两个不同字段。RFC 768:UDP Length 字段


🌟 写在最后

IP 分片最值得掌握的,不是"一个大包拆成几个小包"这句话,而是三个可以动手验证的问题:每片携带哪些字节,怎样记录这些字节的位置,接收端如何证明所有位置都已经填满。

把 3000 字节例子再走一遍:原始载荷 2980,按 MTU 与对齐要求切成 1480、1480、20;每片补上自己的 IP 首部;用相同的归组信息和不同的 Offset、MF 传输;接收端按原始坐标放回,等到没有空洞后再向上交付。

下次在抓包中看到 MF、offset 和多个相同的 ID,可以先画出它们覆盖的字节区间。只要坐标清楚,乱序、缺片、末片先到甚至二次分片,就都有了可以核对的解释。

如果这篇文章帮你理清了某个细节,欢迎点赞、收藏,也欢迎交流你在 Linux 网络编程中遇到的实际现象。我们下一篇见。

相关推荐
小相会自律1 小时前
全平台远程控制软件推荐2026:Win/Mac/Linux/手机/鸿蒙互通哪款好
linux·macos·智能手机·todesk·teamviewer·向日葵
OpenLoong 开源社区1 小时前
打破 NAT 限制,零侵入跨网络通信:基于 WebRTC 的透明 TCP 隧道跨网络通信方案
网络协议·tcp/ip·开源·webrtc·openloong开源社区
风娜1 小时前
【Linux网络】UDP 服务器怎么写?从 socket、bind 到 recvfrom/sendto,手写完整回显服务器
linux·服务器·网络
从琳开始9891 小时前
从Socket到UDP回显服务器:Java网络编程完整指南
java·网络·网络协议·udp·java-ee
静以修身zzz1 小时前
OpenStack对象存储-Swift、OpenStack编排管理-heat、OpenStack网络管理-neutron
linux·运维·服务器·架构·openstack·swift
Lhan.zzZ2 小时前
用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程
c++·vscode·qt·tcp/ip·udp
Gauss松鼠会2 小时前
GaussDB 整体架构解析
运维·服务器·网络·数据库·sql·架构·gaussdb
sinovoip2 小时前
Banana Pi BPI-RT2 WiFi 6 路由器板采用 Realtek RTL8198D 双核 MIPS SoC。
网络·智能路由器
一技安身2 小时前
MySQL 8.4.13 LTS 通用二进制包(Linux免安装版)完整教程
linux·mysql·adb