IP分片

IP 包全长由头部中的 total length 字段决定,该字段共 16 位,因此一个 IP 包最大可达 $2^{16}-1$ ,即 65535 字节。除去头部 20 字节,IP 包最多可承载 $65535-20$ ,即 65515 字节的数据:

当然了,IP 头部如果带有可选选项,长度就不止 20 字节了。这样,它能承载的数据量就要打些折扣,但不会低于 65535-60 ,即 65475 字节。其中,60 是 IP 头部的最大长度。

IP 包需要借助链路层的本地通信能力,搭载在链路层帧中进行传输。典型的以太网帧,最多可以承载 1500 字节的数据。一个长度多达 65535 字节的 IP 包,显然不可能搭载在运输能力只有区区 1500 字节的以太网帧中!那怎么办呢?

基本原理

我们只能将超大的 IP 包分成一个个不超过 1500 的小包,再一一发送出去。各个小包达到目的地后,在目的主机上进行重组,得到原包。这就是所谓的 分片fragmentation )。

那么,一个分片包到到目的主机后,如何判断它属于哪个 IP 包呢?如何判断它是第几个分片呢?

答案就藏在 IP 包的头部中。IP 包头部中有 3 个与分片相关的字段,分别是:

  • 标识符identification ),IP 包的 ID ,全局自增,短时间内不会重复,可唯一标识一个 IP 包;
  • 标志位flags ),包括两个用于控制和识别分片的标志位;
    • DF 标志位禁止中间路由对该包进行分片;
    • MF 标志位表明该包之后还有其他分片;
  • 偏移量fragment offset ),表示一个分片相对于原始 IP 包开头的偏移量,以 8 字节为单位;

假设主机①出口 MTU 是 1500 ,它准备发一个长度为 4000 字节的 IP包给主机②。这个包必须分片:

如上图,原包长达 4000 字节,其中头部 20 字节,数据部分为 3980 字节。分片包最大长度为 1500 ,除去头部的 20 字节,数据部分只剩 1480 。这意味着,原包 3980 字节至少需要分为 3 片。

由于偏移量字段以 8 字节为单位,因此每个分片的数据长度必须为 8 的倍数,最后一片除外。由于 1480 刚好可以被 8 整除,因此分片数据长度可以选择 1480 。

偏移量字段长度只有 13 位,比全长字段的 16 位少了 3 位,因此必须以 8 字节为单位,不然表示范围不够。

第一个分片,包含原包前 1480 字节数据,因此偏移量 offset=0 ;而 MF=1 表示后面还有其他分片。第二个分片,包含原包紧接着的 1480 字节数据,偏移量 $offset={1480\over8}=185$ ;同样 MF=1 表示后面还有其他分片。最后一个分片,包含原包最后 1020 字节数据,偏移量 $offset={2960\over8}=370$ ;而 MF=0 表示它是最后一片了。

这些分片被发出去后,独立在网络中旅行,可能走不同的路由路径,到达时间和顺序也是无法预测的:

分片到达目标主机后,系统根据包头中的源地址、目的地址、标识符、偏移量等字段,将它们重组合成原包。

实际上,系统会分配一块内存作为重组分片的缓冲区。一个分片包首个分片达到后,系统将其移入到该缓冲区,等待其他分片达到:

后续分片达到后,系统先根据源地址、目的地址和标识符确定它属于哪个包;再根据偏移量确定它属于原包的哪个部分;最后将分片数据拼接到原包中。当所有分片都到达后,原包也就成功重组出来了!

如果中间路由链路 MTU 变小,经过的 IP 包大小超出限制,路由便再次对 IP 包进行分片。就算 IP 包已分过片,只要有分片大小超出限制,都要进一步划分:

如上图,路由专线的 MTU 很小。一个去往主机②的 IP 包,被主机①发出前已被分为两片。来到路由器 R1 时,由于第一个分片大小仍超过路由专线 MTU ,路由器 R1 进一步将其分为两片。

中间路由只会对 IP 包进行分片,不会对分片进行重组。如上图, IP 包来到 R2 后链路 MTU 变大,理论上可以对前两个分片进行组装,还原出原来的分片 1 。但出于效率考虑,中间路由不会这么做,分片只有到达目的地即主机②之后,才会开始重组。

如果 IP 包设置了 DF 标志,中间路由便不能将它分片,只能向发送者报告 ICMP 目的不可达 错误。其中,类型为 3 ,表示目的不可达;代码为 4 表示 需要分片,但设置了DF标志 ( fragmentation required, and DF flag set )。

局限性

由于分片可能因丢包而永远无法到达,分片重组必须有超时机制。如果重组无法在一定时间内完成,系统将删除缓冲区中的碎片,放弃重组。因此,如果有分片因路由延迟到达不及时,重组也会失败。

系统用于分片重组的内存是有限的,当大量分片达到导致缓冲区不够用时,系统也会放弃重组。另外,一些别有用心的人,可能会发一些精心构造的分片进行攻击,耗尽系统的重组缓冲区。

基于上述种种原因,编写网络程序时,应该极力避免 IP 分片:

  • 编写 UDP 应用,要严格控制数据报长度,不能超过链路最小 MTU ;
  • 编写 TCP 应用,也要关注本地 MTU 设置,不然可能因中间路由分片导致通信失败;

因分片导致的网络问题,通常比较隐蔽:发少量数据正常;但稍微发多点数据就失败。后续学习 UDP 和 TCP 时,我们还将进一步讨论 IP 分片的问题,并介绍解决问题的最佳实践。

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: