热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

分片传输_IP数据报结构、IP分片的原理与处理

01引言从上一篇文章我们知道了IP数据报的格式,那么这一篇文章就讲解一下IP数据报与IP分片的实现。02IP数据报的数据结构为了描述IP数据报首部的信息,

ed8a0953a1ac8db9a4fc5ac3524fe350.png

01引言

从上一篇文章我们知道了IP数据报的格式,那么这一篇文章就讲解一下IP数据报与IP分片的实现。

02IP数据报的数据结构

为了描述IP数据报首部的信息,LwIP定义了一个ip_hdr的结构体作为描述IP数据报首部,同时还定义了很多获取IP数据报首部的宏定义与设置IP数据报首部的宏定义。

对于代码的实现,有兴趣的就看,没兴趣的就不用管,反正我写的博客也是比较深入的,并没有什么影响...除此之外,还需要注意一点,这些字段是不能使用对齐操作的,因为结构体中的很多字段都是按位进行操作的。因此在LwIP中,使用了 PACK_STRUCT_BEGINPACK_STRUCT_END禁止编译器进行对齐操作。

PACK_STRUCT_BEGIN

/* The IPv4 header */

struct ip_hdr {

 /* 版本 / 首部长度 */

 PACK_STRUCT_FLD_8(u8_t _v_hl);

 /* 服务类型 */

 PACK_STRUCT_FLD_8(u8_t _tos);

 /* 数据报总长度 */

 PACK_STRUCT_FIELD(u16_t _len);

 /* 标识字段 */

 PACK_STRUCT_FIELD(u16_t _id);

 /* 标志与偏移 */

 PACK_STRUCT_FIELD(u16_t _offset);

#define IP_RF 0x8000U        /* 保留的标志位 */

#define IP_DF 0x4000U        /* 不分片标志位 */

#define IP_MF 0x2000U        /* 更多分片标志 */

#define IP_OFFMASK 0x1fffU   /* 用于分段的掩码 */

 /* 生存时间 */

 PACK_STRUCT_FLD_8(u8_t _ttl);

 /* 上层协议*/

 PACK_STRUCT_FLD_8(u8_t _proto);

 /* 校验和 */

 PACK_STRUCT_FIELD(u16_t _chksum);

 /* 源IP地址与目标IP地址 */

 PACK_STRUCT_FLD_S(ip4_addr_p_t src);

 PACK_STRUCT_FLD_S(ip4_addr_p_t dest);

} PACK_STRUCT_STRUCT;

PACK_STRUCT_END

这些字段与我们的IP数据报格式是一样的:

18b80d338468dd7ad846a7ea73237c43.png

除此之外LwIP还定义了很多宏定义对这些数据结构进行操作:

/* 获取IP数据报首部各个字段信息的宏 */

//获取协议版本

#define IPH_V(hdr)  ((hdr)->_v_hl >> 4)

//获取首部长度(字)

#define IPH_HL(hdr) ((hdr)->_v_hl & 0x0f)

//获取获取首部长度字节

#define IPH_HL_BYTES(hdr) ((u8_t)(IPH_HL(hdr) * 4))

//获取服务类型

#define IPH_TOS(hdr) ((hdr)->_tos)

//获取数据报长度

#define IPH_LEN(hdr) ((hdr)->_len)

//获取数据报标识

#define IPH_ID(hdr) ((hdr)->_id)

//获取分片标志位+偏移量

#define IPH_OFFSET(hdr) ((hdr)->_offset)

//获取偏移量大小(字节)

#define IPH_OFFSET_BYTES(hdr) \

((u16_t)((lwip_ntohs(IPH_OFFSET(hdr)) & IP_OFFMASK) * 8U))

//获取生存时间

#define IPH_TTL(hdr) ((hdr)->_ttl)

//获取上层协议

#define IPH_PROTO(hdr) ((hdr)->_proto)

//获取校验和

#define IPH_CHKSUM(hdr) ((hdr)->_chksum)

/* 用于填写IP数据报首部的宏*/

//设置版本号跟首部长度

#define IPH_VHL_SET(hdr, v, hl) \

(hdr)->_v_hl &#61; (u8_t)((((v) <<4) | (hl)))

//设置服务类型

#define IPH_TOS_SET(hdr, tos) (hdr)->_tos &#61; (tos)

//设置数据报总长度

#define IPH_LEN_SET(hdr, len) (hdr)->_len &#61; (len)

//设置标识

#define IPH_ID_SET(hdr, id) (hdr)->_id &#61; (id)

//设置分片标志与偏移量

#define IPH_OFFSET_SET(hdr, off) (hdr)->_offset &#61; (off)

//设置生存时间

#define IPH_TTL_SET(hdr, ttl) (hdr)->_ttl &#61; (u8_t)(ttl)

//设置上层协议

#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto &#61; (u8_t)(proto)

//设置校验和

#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum &#61; (chksum)

03IP数据报分片

其实在讲解IP数据报的时候也讲解了IP数据报的分片&#xff0c;因为任何一个IP数据报都是依赖网卡进行发送的&#xff0c;而对于某些网卡&#xff0c;它所能承载的IP数据报大小是有限的&#xff0c;一个链路层帧能承载的最大数据量叫做最大传送单元(Maximum Transmission Unit&#xff0c;MTU)&#xff0c;比如以太网的MTU就是1500字节数据&#xff0c;而某些广域网链路的帧可承载不超过576字节的数据。每个IP数据报都必须封装在链路层帧中从一台设备传输到下一台设备&#xff0c;这些设备可以是主机也可以是路由器&#xff0c;故链路层协议的MTU严格地限制着IP数据报的长度。

说点题外话&#xff1a;IP分片功能只在IPv4中实现&#xff0c;而在IPv6是不允许IP层进行分片处理的&#xff0c;那么就需要主机确定链路的MTU大小。

IP分片是在IP层完成的&#xff0c;假设一个IP数据报在主机中没有分片&#xff0c;但不代表它不会在中间传输的过程中不进行分片&#xff0c;假设这个IP数据报从主机的以太网出来&#xff0c;携带了1460个字节的数据&#xff0c;但是它在转发的过程中&#xff0c;遇到了一个MTU只有576个字节的网卡设备&#xff0c;那么它必须进行分片才能通过这个网卡&#xff0c;因此&#xff0c;它将会在这个设备中进行分片然后再向目的地进军....

对IP数据报长度具有严格限制并不是主要问题&#xff0c;问题在于在发送方与目的地路径上的每段链路可能使用不同的链路层协议&#xff0c;并且每种协议可能具有不同的MTU&#xff0c;这就需要有一个很好的处理方式&#xff0c;随之而来的就是IP数据报分片处理。其实IP分片在很多书上也叫IP分组&#xff0c;我个人还是喜欢叫IP分片。

分片处理是将IP数据报中的数据分片成两个或更多个较小的IP数据报&#xff0c;用单独的链路层帧封装这些较小的IP数据报&#xff1b;然后向输出链路上发送这些帧&#xff0c;每个这些较小的数据报都称为分片&#xff0c;由于IP数据报的分片偏移量是用8的整数倍记录的&#xff0c;所以每个数据报中的分片数据大小也必须是8的整数倍。

所有分片数据包在其到达目的地传输层之前需要在IP层完成重新组装(也称之为重装/重组)。但是如果在每个中间转发设备的IP层中组装分片数据包&#xff0c;那么将严重影响路由器的性能。

例如一台路由器&#xff0c;在收到数据分片后又进行重装完成后再转发&#xff0c;这样子的处理简直就是浪费生命&#xff0c;所以 IPv4的设计者决定将数据报的重新组装工作放到端系统中&#xff0c;而不是放到网络路由器中&#xff0c;什么是端系统呢&#xff1f;简单来说就是数据包中的目标IP地址的主机&#xff0c;在这台机器上的IP层进行数据分片的重装&#xff0c;这样子数据分片可以任意在各个路由之间进行转发&#xff0c;而路由器就无需理会数据分片是在哪里重装&#xff0c;只要数据分片不是给路由器的&#xff0c;那么就将其转发出去即可&#xff0c;当然&#xff0c;这样子的处理就会是的每个数据分片到达目标IP地址的主机时间是不一样的。因此&#xff0c;IP分片到达目标主机的顺序也是不确定的&#xff0c;在目标主机中必须进行重装的处理&#xff0c;还要设定重装的超时时间。

那么怎么样处理每个分片的数据呢&#xff1f;其实在发送主机中&#xff0c;它会把需要分片的数据进行切割(分片)&#xff0c;按照数据的偏移量进行切割&#xff0c;切割后形成的每个IP数据报(即分片)具有与初始IP数据报几乎一样的IP数据报首部&#xff0c;为什么说是几乎一样而不是全部一样呢&#xff0c;因为IP数据报首部的标志、分片偏移量这两个字段与分片有关&#xff0c;不同的分片&#xff0c;这些信息可能不一样&#xff0c;不同的分片数据报长度也是不一样的&#xff0c;校验和字段也是不一样的。但是源IP地址、目标IP地址与标识号肯定是一样的&#xff0c;每个分片上的分片偏移量字段是不一样的。与IP分片有关的标志位&#xff1a;

/* 标识字段 */

PACK_STRUCT_FIELD(u16_t _id);

/* 标志与偏移 */

PACK_STRUCT_FIELD(u16_t _offset);

#define IP_RF 0x8000U        /* 保留的标志位 */

#define IP_DF 0x4000U        /* 不分片标志位 */

#define IP_MF 0x2000U        /* 更多分片标志 */

标识字段用于表示IP层发送出去的每一份IP数据报&#xff0c;在发送每一份报文&#xff0c;该值加1&#xff0c;在分片的时候&#xff0c;该字段会被复制到每个分片数据报中&#xff0c;在目标接收主机中&#xff0c;使用该字段判断这些数据是否属于同一个IP数据报。

标志位(3bit)的定义如下&#xff1a;第一位保留未用&#xff1b;第二位是不分片标志位&#xff0c;如果该位为1&#xff0c;则表示IP数据报在发送的过程中不允许进行分片&#xff0c;如果这个IP数据报的大小超过链路层能承载的大小&#xff0c;这个IP数据报将被丢弃&#xff0c;如果该位为0则表示IP层在必要的时候可以对其进行分片处理&#xff1b;第三位为更多分片位&#xff0c;如果为1则表示该分片数据报不是整个IP数据报的最后一个分片&#xff0c;如果为0则表示是整个IP数据报的最后一个分片。

分片偏移量占据13bit空间&#xff0c;表示当前分片所携带的数据在整个IP数据报中的相对偏移位置(以8字节为单位)&#xff0c;目标主机必须受到以0偏移量开始到最高偏移量的所有分片&#xff0c;才能将分片进行重装为一个完整的IP数据报&#xff0c;并且重装IP数据报的依据就是分片的偏移量。

IP协议是一种提供不可靠的传输服务协议&#xff0c;一个或多个分片可能永远到达不了目的地。为了让目标主机相信它已经收到了初始IP数据报的最后一个分片&#xff0c;在最后一个IP分片上的标志字段 IP_MF会被设置为0。而所有其他分片的标志被设为1。另外&#xff0c;为了让目的主机确定是否丢失了一个分片(且能按正确的顺序重新组装分片)&#xff0c;使用分片偏移量字段指定该分片应放在初始IP数据报的哪个位置。

比如一个主机打算发送4000字节的IP数据报(20字节IP首部加上3980字节IP数据区域&#xff0c;假设没有IP数据报首部选项字段)&#xff0c;且该数据报必须通过一条MTU为1500字节的以太网链路。这就意味着源始IP数据报中3980字节数据必须被分配为3个独立的数据报分片(其中的每个分片也是一个IP数据报)。假定初始IP数据报贴上的标识号为666&#xff0c;那么第一个分片的数据报总大小为1500字节(1480字节数据大小&#43;20字节IP数据报首部)&#xff0c;分片偏移量为0&#xff0c;第二个分片的数据报大小也为1500字节&#xff0c;分片偏移量为185(185*8&#61;1480)&#xff0c;第三个分片的数据报大小为1020(4000-1480-1480&#43;20)&#xff0c;分片偏移量为370(185&#43;185)。

那么对于的IP分片数据结构就是如下&#xff1a;

编号标识IP_RFIP_DFIP_MF偏移量携带数据大小
原报文666保留0004000
分片1666保留0101500
分片2666保留011851500
分片3666保留003701020
05LwIP源码实现IP分片

那么LwIP源码是怎么样实现的呢&#xff1f;

整个函数是比较复杂的&#xff0c;主要是循环处理数据报的分片&#xff0c;主要是处理偏移量与分片标志&#xff0c;拷贝原始数据的部分到分片空间中并发送出去&#xff0c;然后填写IP数据报首部的其他字段&#xff0c;如果是分片的最后一个数据报&#xff0c;则修改标志位并且发送出去&#xff0c;发送完成则释放分片空间。

err_t

ip4_frag(struct pbuf *p,

struct netif *netif,

const ip4_addr_t *dest)

{

 struct pbuf *rambuf;

 struct pbuf *newpbuf;

 u16_t newpbuflen &#61; 0;

 u16_t left_to_copy;

 struct ip_hdr *original_iphdr;

 struct ip_hdr *iphdr;

 const u16_t nfb &#61; (u16_t)((netif->mtu - IP_HLEN) / 8);

 u16_t left, fragsize;

 u16_t ofo;

 int last;

 u16_t poff &#61; IP_HLEN;

 u16_t tmp;

 int mf_set;

 //原来的数据区域

 original_iphdr &#61; (struct ip_hdr *)p->payload;

 iphdr &#61; original_iphdr;

 if (IPH_HL_BYTES(iphdr) !&#61; IP_HLEN) {

   /* 如果ip4_frag不支持IP选项 */

   return ERR_VAL;

 }

 /* 保存原始偏移量 */

 tmp &#61; lwip_ntohs(IPH_OFFSET(iphdr));

 ofo &#61; tmp & IP_OFFMASK;

 /* 得到更多的分配标志位 */

 mf_set &#61; tmp & IP_MF;

 /* 得到要发送数据的长度 */

 left &#61; (u16_t)(p->tot_len - IP_HLEN);

 //要发送的数据长度大于0

 while (left)

 {

   fragsize &#61; LWIP_MIN(left, (u16_t)(nfb * 8));//4000  1480

   //申请分片pbuf结构

   rambuf &#61; pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);

   if (rambuf &#61;&#61; NULL) {

     goto memerr;

   }

   LWIP_ASSERT("this needs a pbuf in one piece!",

               (rambuf->len >&#61; (IP_HLEN)));

   //拷贝原始数据的部分到分片中

   SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);

   //得到分片包存储区域

   iphdr &#61; (struct ip_hdr *)rambuf->payload;

   //更新还需要拷贝的数据

   left_to_copy &#61; fragsize;

   while (left_to_copy)

   {

     struct pbuf_custom_ref *pcr;

     //定义记录已经拷贝的数据大小变量 plen

     u16_t plen &#61; (u16_t)(p->len - poff);

     //需要创建一个新pbuf拷贝剩下的

     newpbuflen &#61; LWIP_MIN(left_to_copy, plen);

     if (!newpbuflen)

     {

       poff &#61; 0;

       p &#61; p->next;

       continue;

     }

     //申请分片新的pbuf

     pcr &#61; ip_frag_alloc_pbuf_custom_ref();

     if (pcr &#61;&#61; NULL)

     {

       pbuf_free(rambuf);

       goto memerr;

     }

     /* 初始化这个pbuf */

     newpbuf &#61; pbuf_alloced_custom(PBUF_RAW,

                                   newpbuflen,

                                   PBUF_REF,

                                   &pcr->pc,

                                   (u8_t *)p->payload &#43; poff,

                                   newpbuflen);

     if (newpbuf &#61;&#61; NULL)

     {

       ip_frag_free_pbuf_custom_ref(pcr);

       pbuf_free(rambuf);

       goto memerr;

     }

     pbuf_ref(p);

     pcr->original &#61; p;

     pcr->pc.custom_free_function &#61; ipfrag_free_pbuf_custom;

     //将它添加到rambuf链的末尾

     pbuf_cat(rambuf, newpbuf);

     left_to_copy &#61; (u16_t)(left_to_copy - newpbuflen);

     if (left_to_copy)

     {

       poff &#61; 0;

       p &#61; p->next;

     }

   }

   //更新数据报的偏移量

   poff &#61; (u16_t)(poff &#43; newpbuflen);

   /* 处理分片 */

   last &#61; (left <&#61; netif->mtu - IP_HLEN);

   /* 设置新的偏移和MF标志 */

   tmp &#61; (IP_OFFMASK & (ofo));

   if (!last || mf_set)

   {

     tmp &#61; tmp | IP_MF;

   }

   //填写分片相关字段

   IPH_OFFSET_SET(iphdr, lwip_htons(tmp));

   IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize &#43; IP_HLEN)));

   IPH_CHKSUM_SET(iphdr, 0);

#if CHECKSUM_GEN_IP

   //校验和

   IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {

     IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN));

   }

#endif

   /* 发送数据报 */

   netif->output(netif, rambuf, dest);

   IPFRAG_STATS_INC(ip_frag.xmit);

   //释放分片空间

   pbuf_free(rambuf);

   //待发送数据减少

   left &#61; (u16_t)(left - fragsize);

   //分片偏移增加

   ofo &#61; (u16_t)(ofo &#43; nfb);

 }

 MIB2_STATS_INC(mib2.ipfragoks);

 return ERR_OK;

memerr:

 MIB2_STATS_INC(mib2.ipfragfails);

 return ERR_MEM;

}

如果你耐心看到这里&#xff0c;我感觉很欣慰&#xff0c;因为写文章不容易&#xff0c;特别是写这些比较深入的文章&#xff0c;看文章更不容易&#xff0c;特别是对暂时不需要的人。。。当然啦&#xff0c;我写文章并不是为了什么东西~我只是为了写而写。

热情提示&#xff1a;在公众号上不适合看源代码&#xff0c;如需认真研究源码的请移步到博客上看&#xff1a; https://jiejietop.cn/index.php/2019/04/10/ipfp/&#xff0c;源码可以全屏&#xff0c;高亮看起来更舒服...

b9e53bfaadbd1e5724da533eee4ea3aa.png

0e5829817bc1e6bafd5420cea0d37bbb.png你点的每个赞&#xff0c;我都认真当成了喜欢



推荐阅读
  • 深入理解Redis的数据结构与对象系统
    本文详细探讨了Redis中的数据结构和对象系统的实现,包括字符串、列表、集合、哈希表和有序集合等五种核心对象类型,以及它们所使用的底层数据结构。通过分析源码和相关文献,帮助读者更好地理解Redis的设计原理。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文基于刘洪波老师的《英文词根词缀精讲》,深入探讨了多个重要词根词缀的起源及其相关词汇,帮助读者更好地理解和记忆英语单词。 ... [详细]
  • 前言--页数多了以后需要指定到某一页(只做了功能,样式没有细调)html ... [详细]
  • 基因组浏览器中的Wig格式解析
    本文详细介绍了Wiggle(Wig)格式及其在基因组浏览器中的应用,涵盖variableStep和fixedStep两种主要格式的特点、适用场景及具体使用方法。同时,还提供了关于数据值和自定义参数的补充信息。 ... [详细]
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
  • 本文详细介绍了Java中org.neo4j.helpers.collection.Iterators.single()方法的功能、使用场景及代码示例,帮助开发者更好地理解和应用该方法。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 本文详细介绍了 Dockerfile 的编写方法及其在网络配置中的应用,涵盖基础指令、镜像构建与发布流程,并深入探讨了 Docker 的默认网络、容器互联及自定义网络的实现。 ... [详细]
  • 本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ... [详细]
  • 本文讨论了如何根据特定条件动态显示或隐藏文件上传控件中的默认文本(如“未选择文件”)。通过结合CSS和JavaScript,可以实现更灵活的用户界面。 ... [详细]
  • 本教程涵盖OpenGL基础操作及直线光栅化技术,包括点的绘制、简单图形绘制、直线绘制以及DDA和中点画线算法。通过逐步实践,帮助读者掌握OpenGL的基本使用方法。 ... [详细]
  • HBase运维工具全解析
    本文深入探讨了HBase常用的运维工具,详细介绍了每种工具的功能、使用场景及操作示例。对于HBase的开发人员和运维工程师来说,这些工具是日常管理和故障排查的重要手段。 ... [详细]
  • This document outlines the recommended naming conventions for HTML attributes in Fast Components, focusing on readability and consistency with existing standards. ... [详细]
  • 本文详细解释了华为ENSP模拟器中常用的命令,涵盖用户模式、系统模式、接口模式和地址池视图模式下的操作。这些命令对于进行计算机网络实验至关重要,帮助用户更好地理解和配置路由器及PC机的通信。 ... [详细]
author-avatar
曾雅芬珍念孟璇
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有