IP协议分片&&重组问题

分片是什么&&为什么会有分片

IP数据报分片的主要目的是为了防止IP数据报文长度超过下一跳链路MTU(最大传输单元)。

数据链路层之MTU

  • 数据链路层中有一个东西叫做MTU(最大传输单元),它的作用主要是控制上层给的数据报不要太大,大的数据报文在网络内传输时会占用过多带宽资源,使其它报文的转发效率下降,通过MTU来限制数据报文,根据MTU将数据报文分片以减少数据碰撞的发生。

  • 每种数据链路的最大传输单元 MTU 都是不相同的,如 FDDI 数据链路 MTU 4352、以太网的 MTU 是 1500 字节等。

  • 每种数据链路的 MTU 之所以不同,是因为每个不同类型的数据链路的使用目的不同。使用目的不同,可承载的 MTU 也就不同。

  • 其中,我们最常见数据链路是以太网,它的 MTU 是 1500 字节。

  • 那么当 IP 数据包大小大于 MTU 时, IP 数据包就会被分片。

  • 经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行,路由器是不会进行重组的。

  • 假设发送方发送一个 4000 字节的大数据报,若要传输在以太网链路,则需要把数据报分片成 3 个小数据报进行传输,再交由接收方重组成大数据报。即1500,1500,1000三个数据报

工作原理

得先了解报文结构

IP报文特性

IP 包全长由头部中的 总长度 字段决定,该字段共 16 位,因此一个 IP 包最大可达 65535 字节。除去头部 20 字节,IP 包最多可承载 65515 字节的数据:

在这里插入图片描述

如果IP 头部带有可选选项,长度就不止 20 字节了,但最大60字节。以上关于IP报文详细结构可以看这篇。,同理,数据的携带就会相应变少。

在观察IP数据报文的特性之后我们发现:

  • 一个 65535 字节的 IP 报文,显然不可能在运输能力只有 1500 字节的以太网帧或任意一个数据链路层协议帧中。

发送端分片简单原理

在这里插入图片描述
我们从上至下讲:

  • TCP报文询问IP,IP询问数据链路层MTU是多少,数据链路层返回1500。
  • IP得到MTU=1500,因为IP报头需要最少20字节,所以,给TCP的回复是1480。
  • TCP得到回复MTU=1280,由于TCP报头最少要20字节,故根据MTU,TCP留给数据的空间就是1460字节。
  • TCP再根据对端发来的接收端窗口大小,if 接收端窗口.size() < MTU.size() ,则最终TCP数据报文的大小 = 接收端窗口大小,else TCP数据报文的大小 = 1460。
  • 说到这里大致就明朗了,IP其实没有进行分片的机会!!!为什么呢?
    • 主要是因为分片降低网络性能
      • TCP在传输层通过滑动窗口以及和对端商量好了发送数据的大小,你IP又在网络层分一次,干嘛不直接在传输层做完呢,这就可以提高效率了。TCP两件事都给做了,这个过程叫做分段。

      • 另外,如果IP分片了,数据在传输过程中出现了丢包,可TCP是IP的上层,是不知道IP分片了,分了几片的,所以触发了超时重传等机制,TCP就不得不把整个丢失的包进行重发,但实际丢掉的是IP分片之后的一部分数据包。那么,网络设计者觉得与其让IP分片,不如TCP把这事做了,丢包了也只需要重传一小部分,而不是像上面那样重传整个数据包。

      • 同时,如果在IP分片,每一个分片又得带上相差无几的报头,浪费!

      • 所以,不如在传输层全部做好,IP只需要做一件事情——>添加自己的报头,转发报文给数据链路层就完成工作了!!!

      • 所以,TCP把分片的工作给抢了,这种机制叫做MSS(最大分段大小),防止IP分片

        故此,提出一个概念:

        • IP协议负责数据包的地址标注与传输,他是传输策略的执行者
        • TCP协议则建立连接,管理流量和错误校验,保证了数据传输的可靠性,他是传输策略的制定者

发送端分片详细原理

这里强烈建议去熟悉IP报文结构
IP 包头部中有 3 个与分片相关的字段,分别是:
在这里插入图片描述

  • 16位标识: IP 包的 ID ,全局自增,短时间内不会重复,主机发送的报文的唯一标识. 如果IP报文在数据链路层被分片了, 那么每一个片里面的这个id都是相同的.
  • 3位标志位:
    • 第一位:DF - Don’t Fragment 位,为1表示禁止分片, 这时候如果报文长度超过MTU, IP模块就会丢弃报文.
    • 第二位:MF - More Fragments 位,该位用于指示报文是否有后续分片。如果分片了的话, 最后一个分片置为0, 其他是1, 类似于一个结束标记。
    • 第三位:Fragment Offset 位,这些位标识分片在原始报文中的相对位置。它们与分片序号一起,用于重组时排序和定位分片。
  • 13位片偏移: 是分片相对于原始IP报文开始处的偏移. 其实就是在表示当前分片在原报文中处在哪个位置. 实际偏移的字节数是这个值 * 8 得到的. 因此, 除了最后一个报文之外, 其他报文的长度必须是8的整数倍(否则报文就不连续了).
    • 这里解释一下为什么是13位片偏移:

      IP报头中的片偏移字段只有13位,如果以字节为单位,其能表示的偏移范围是:

      2^13 = 8191字节 = 8KB

      而IP数据报的总长度字段有16位,可以表示的总长度是:

      2^16 = 65535字节 = 64KB

      以字节为偏移单位,13位能表示的最大偏移量是8KB,如果数据报大于8KB,偏移字段会出现表示不足的情况。

      但偏移量以8字节为单位,13位就可以表示:

      8192 * 8 = 64KB

      正好与总长度字段的表示范围相同。

      所以出于表示范围的限制,偏移字段必须采用“以8字节为单位”的设计,即使最小分片也会多占用一点空间,这是对范围表示和分片处理的一个综合考量。

假设发送端通过以太网帧 MTU 是 1500 ,它准备发一个长度为 4000 字节的 IP包。TCP分片情况如下:

在这里插入图片描述

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

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

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

接收端是如何重组IP报文的

这些分片被发出去后,由于是不同的数据包,可能出现丢包,阻塞等现象,到达时间和顺序是无法预测的,所以,无法按照分片到达顺序来确定。

因此,分片到达目标主机后,系统根据报头中字段,将它们重组。

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

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

IP是否有可能分片

如果中间路由链路 MTU 变小,经过的 IP 包大小超出限制,路由便再次对 IP 包进行分片。就算 IP 包已分过片,只要有分片大小超出限制,都要进一步划分(注意按照现在的讲法IP此时是可以分片的):

在这里插入图片描述

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

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

但我们说过,IP分片会导致效率的下降 ,那么如何让IP分不了包呢?(以下PMTU内容参考小白debug

  • 获取 PMTU(整个IP报文传输过程中设备数据链路层的最小MTU)!!!
  • 使用 IP 报头中的3位标志位
  • IP 包设置 DF 标志,中间路由便不能将它分片,只能向发送者报告 ICMP 目的不可达 错误。
  • ICMP中包含PMTU信息,TCP获取PMTU后,重新组织数据段发送,以此避免IP分片!
    在这里插入图片描述
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现一个完整的IP协议可能需要大量的代码和深入的网络知识,下面是一个简单的IP协议的示例代码,仅供参考。 ```c++ #include <iostream> #include <vector> #include <string> #include <unordered_map> using namespace std; // IP地址结构体 struct IPAddress { unsigned char addr[4]; // 打印IP地址 void print() { cout << (int)addr[0] << "." << (int)addr[1] << "." << (int)addr[2] << "." << (int)addr[3] << endl; } }; // IP数据包结构体 struct IPData { unsigned short version; // 版本号 unsigned short header_len; // 头部长度 unsigned short total_len; // 总长度 unsigned short id; // 数据包ID unsigned short flags; // 标志位 unsigned short offset; // 片偏移 unsigned char ttl; // 生存时间 unsigned char protocol; // 协议类型 unsigned short checksum; // 校验和 IPAddress src_addr; // 源IP地址 IPAddress dest_addr; // 目标IP地址 string data; // 数据 // 打印IP数据包信息 void print() { cout << "Version: " << version << endl; cout << "Header Length: " << header_len << endl; cout << "Total Length: " << total_len << endl; cout << "ID: " << id << endl; cout << "Flags: " << flags << endl; cout << "Offset: " << offset << endl; cout << "TTL: " << (int)ttl << endl; cout << "Protocol: " << (int)protocol << endl; cout << "Checksum: " << checksum << endl; cout << "Source Address: "; src_addr.print(); cout << "Destination Address: "; dest_addr.print(); cout << "Data: " << data << endl; } }; // 分片函数 vector<IPData> fragment(IPData ip_data, unsigned short mtu) { vector<IPData> fragments; // 判断是否需要分片 if (ip_data.total_len <= mtu) { fragments.push_back(ip_data); return fragments; } // 计算分片数 int num_fragments = (int)(ip_data.total_len / mtu); if (ip_data.total_len % mtu != 0) { num_fragments++; } // 分片 for (int i = 0; i < num_fragments; i++) { IPData fragment = ip_data; fragment.total_len = mtu; fragment.offset = i * mtu / 8; fragment.flags = (i == num_fragments - 1) ? 0 : 1; fragments.push_back(fragment); } return fragments; } // 重组函数 IPData reassemble(vector<IPData> fragments) { IPData ip_data = fragments[0]; // 检查标志位 bool is_last_fragment = false; for (int i = 0; i < fragments.size(); i++) { if (fragments[i].flags == 0) { is_last_fragment = true; break; } } // 拼接数据 for (int i = 0; i < fragments.size(); i++) { ip_data.data += fragments[i].data; } // 更新IP数据包信息 ip_data.total_len = ip_data.data.size() + sizeof(IPData); ip_data.flags = is_last_fragment ? 0 : 1; return ip_data; } // 路由选择函数 IPAddress route_select(IPAddress src_addr, IPAddress dest_addr) { // 简单的路由选择,根据目标地址的第一个字节选择网络 int network = dest_addr.addr[0]; if (network == 192) { return IPAddress{{192, 168, 0, 1}}; } else if (network == 10) { return IPAddress{{10, 0, 0, 1}}; } else { return IPAddress{{127, 0, 0, 1}}; } } // 地址解析函数 unordered_map<IPAddress, string> arp_cache; string address_resolution(IPAddress ip_addr) { if (arp_cache.find(ip_addr) != arp_cache.end()) { return arp_cache[ip_addr]; } else { return "unknown"; } } int main() { // 测试分片函数 IPData ip_data; ip_data.total_len = 1500; vector<IPData> fragments = fragment(ip_data, 1000); cout << "Number of fragments: " << fragments.size() << endl; for (int i = 0; i < fragments.size(); i++) { fragments[i].print(); } // 测试重组函数 IPData reassembled = reassemble(fragments); reassembled.print(); // 测试路由选择函数 IPAddress src_addr{{192, 168, 0, 2}}; IPAddress dest_addr{{10, 0, 0, 1}}; IPAddress next_hop = route_select(src_addr, dest_addr); cout << "Next hop: "; next_hop.print(); // 测试地址解析函数 arp_cache[IPAddress{{192, 168, 0, 1}}] = "00:11:22:33:44:55"; string mac_addr = address_resolution(IPAddress{{192, 168, 0, 1}}); cout << "MAC Address: " << mac_addr << endl; return 0; } ``` 该代码实现了IP协议中的分片重组、路由选择和地址解析功能,其中: - 分片函数 `fragment` 接收一个 IP 数据包和 MTU 值,根据 MTU 值计算需要成多少片,然后将 IP 数据包成多个片段,并设置片偏移、标志位等信息。 - 重组函数 `reassemble` 接收多个 IP 数据包片段,检查标志位,组合数据,并更新 IP 数据包的信息。 - 路由选择函数 `route_select` 接收源 IP 地址和目标 IP 地址,在本例中根据目标 IP 地址的第一个字节选择网络,并返回下一跳的 IP 地址。 - 地址解析函数 `address_resolution` 接收一个 IP 地址,根据 ARP 缓存返回对应的 MAC 地址。 请注意,该代码仅供参考,实际实现可能需要更多的代码和深入的网络知识,如需用于实际应用,请根据实际情况进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值