星光编译者头像
【Linux 网络编程】IP 分片与重组:3000 字节的数据报,怎样拆开又拼回来?封面图

【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 与层次说清楚
  2. ID、DF、MF 和片偏移,各自回答什么
  3. 完整算一次:3000 字节如何分成三片
  4. 每片独立传输,哪些首部会重复
  5. 接收端怎样组装:归组、定位与判断收齐
  6. 已经分片,还能再次分片吗
  7. 丢一片为什么影响整份数据,以及怎样减少分片
  8. 离线实验:模拟乱序、缺片与二次分片
  9. 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 为例:

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 归组。基础协议层面的重组标识是:

源 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 与片偏移放在一起,才好判断:

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

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

在这里插入图片描述

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

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

两个方向的公式要分清:

字节起点 = 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 字节”本身的歧义

本节明确约定:

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

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

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

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

1500 - 20 = 1480 字节

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

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

非末片最大载荷 = floor((MTU - H) / 8) × 8

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

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

3.3 按原始载荷顺序切三段

2980 字节载荷切分为:

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

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

分片原始载荷区间本片载荷本片 IP 总长Offset 字段值MFID
A[0, 1480)14801500014242
B[1480, 2960)1480150018514242
C[2960, 2980)204037004242

在这里插入图片描述

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

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

第一,载荷守恒:

1480 + 1480 + 20 = 2980

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

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

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

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 首部

把原始数据报写成:

IPv4 首部 | UDP 首部 | 应用数据

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

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

原始 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 放到原来的位置

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

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

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

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

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

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

原始载荷总长 = 末片 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 字节,则非末片的最大载荷为:

floor((620 - 20) / 8) × 8 = 600 字节

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

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

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

B1:1480          → Offset = 185
B2:1480 + 600    → Offset = 260
B3:1480 + 1200   → Offset = 335

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

新 Offset = 旧 Offset + 在旧片载荷内的字节偏移 / 8

在这里插入图片描述

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

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

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

新片原始载荷区间载荷IP 总长OffsetMF
B1[1480, 2080)6006201851
B2[2080, 2680)6006202601
B3[2680, 2960)2803003351

概括二次分片的 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 片全部到达的概率为:

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 示例里,经常看到:

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 标准库,不发送网络报文,也不修改系统配置。从文章目录运行:

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 的预期关键输出为:

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:

0、75、150、185、260、335、370

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

8.3 核心计算其实很短

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

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 替换成你自己的测试环境;这个地址只是文档示例,不是可直接探测的公共服务。

# 查看接口概况,并查询去往测试目标的出口
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 请求中:

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 字段条件:

# 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。

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

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,观察另一种处理路径

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

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 网络编程中遇到的实际现象。我们下一篇见。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_64304261/article/details/169549543

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--