热门标签 | 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;我都认真当成了喜欢



推荐阅读
  • 在1995年,Simon Plouffe 发现了一种特殊的求和方法来表示某些常数。两年后,Bailey 和 Borwein 在他们的论文中发表了这一发现,这种方法被命名为 Bailey-Borwein-Plouffe (BBP) 公式。该问题要求计算圆周率 π 的第 n 个十六进制数字。 ... [详细]
  • 如何高效解决Android应用ANR问题?
    本文介绍了ANR(应用程序无响应)的基本概念、常见原因及其解决方案,并提供了实用的工具和技巧帮助开发者快速定位和解决ANR问题,提高应用的用户体验。 ... [详细]
  • 本文将深入探讨 Unreal Engine 4 (UE4) 中的距离场技术,包括其原理、实现细节以及在渲染中的应用。距离场技术在现代游戏引擎中用于提高光照和阴影的效果,尤其是在处理复杂几何形状时。文章将结合具体代码示例,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 题目编号:2049 [SDOI2008]Cave Exploration。题目描述了一种动态图操作场景,涉及三种基本操作:断开两个节点间的连接(destroy(a,b))、建立两个节点间的连接(connect(a,b))以及查询两节点是否连通(query(a,b))。所有操作均确保图中无环存在。 ... [详细]
  • 探讨了在HTML表单中使用元素代替进行表单提交的方法。 ... [详细]
  • 二维码的实现与应用
    本文介绍了二维码的基本概念、分类及其优缺点,并详细描述了如何使用Java编程语言结合第三方库(如ZXing和qrcode.jar)来实现二维码的生成与解析。 ... [详细]
  • 洛谷 P4009 汽车加油行驶问题 解析
    探讨了经典算法题目——汽车加油行驶问题,通过网络流和费用流的视角,深入解析了该问题的解决方案。本文将详细阐述如何利用最短路径算法解决这一问题,并提供详细的代码实现。 ... [详细]
  • Go从入门到精通系列视频之go编程语言密码学哈希算法(二) ... [详细]
  • 深入解析WebP图片格式及其应用
    随着互联网技术的发展,无论是PC端还是移动端,图片数据流量占据了很大比重。尤其在高分辨率屏幕普及的背景下,如何在保证图片质量的同时减少文件大小,成为了亟待解决的问题。本文将详细介绍Google推出的WebP图片格式,探讨其在实际项目中的应用及优化策略。 ... [详细]
  • protobuf 使用心得:解析与编码陷阱
    本文记录了一次在广告系统中使用protobuf进行数据交换时遇到的问题及其解决过程。通过这次经历,我们将探讨protobuf的特性和编码机制,帮助开发者避免类似的陷阱。 ... [详细]
  • 线段树详解与实现
    本文详细介绍了线段树的基本概念及其在编程竞赛中的应用,并提供了一个具体的线段树实现代码示例。 ... [详细]
  • 本题涉及一个长度为n的序列{ai},代表一系列树木的美学价值。任务是处理m个查询,每个查询提供三个参数l、r和P,目标是在所有满足l < l' ... [详细]
  • JavaScript 跨域解决方案详解
    本文详细介绍了JavaScript在不同域之间进行数据传输或通信的技术,包括使用JSONP、修改document.domain、利用window.name以及HTML5的postMessage方法等跨域解决方案。 ... [详细]
  • 本文详细介绍了在Luat OS中如何实现C与Lua的混合编程,包括在C环境中运行Lua脚本、封装可被Lua调用的C语言库,以及C与Lua之间的数据交互方法。 ... [详细]
  • 本文探讨了异步编程的发展历程,从最初的AJAX异步回调到现代的Promise、Generator+Co以及Async/Await等技术。文章详细分析了Promise的工作原理及其源码实现,帮助开发者更好地理解和使用这一重要工具。 ... [详细]
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社区 版权所有