从上一篇文章我们知道了IP数据报的格式,那么这一篇文章就讲解一下IP数据报与IP分片的实现。
02IP数据报的数据结构为了描述IP数据报首部的信息,LwIP定义了一个ip_hdr的结构体作为描述IP数据报首部,同时还定义了很多获取IP数据报首部的宏定义与设置IP数据报首部的宏定义。
对于代码的实现,有兴趣的就看,没兴趣的就不用管,反正我写的博客也是比较深入的,并没有什么影响...除此之外,还需要注意一点,这些字段是不能使用对齐操作的,因为结构体中的很多字段都是按位进行操作的。因此在LwIP中,使用了 PACK_STRUCT_BEGIN
与 PACK_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数据报格式是一样的:
除此之外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_RF | IP_DF | IP_MF | 偏移量 | 携带数据大小 |
---|---|---|---|---|---|---|
原报文 | 666 | 保留 | 0 | 0 | 0 | 4000 |
分片1 | 666 | 保留 | 0 | 1 | 0 | 1500 |
分片2 | 666 | 保留 | 0 | 1 | 185 | 1500 |
分片3 | 666 | 保留 | 0 | 0 | 370 | 1020 |
那么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;高亮看起来更舒服...
你点的每个赞&#xff0c;我都认真当成了喜欢