LwIP中IP数据报的实现(1)——IP数据报分片

IP数据报的数据结构

为了描述IP数据报首部的信息,LwIP定义了一个ip_hdr的结构体作为描述IP数据报首部,同时还定义了很多获取IP数据报首部的宏定义与设置IP数据报首部的宏定义:在定义结构体的时候要使用PACK_STRUCT_BEGINPACK_STRUCT_END禁止编译器进行对齐操作,因为结构体中的很多字段都是按位进行操作的。

/* IPv4 首部数据结构 */
PACK_STRUCT_BEGIN
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数据报首部各个字段信息的宏 */

//获取协议版本
#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 = (u8_t)((((v) << 4) | (hl)))
//设置服务类型
#define IPH_TOS_SET(hdr, tos) (hdr)->_tos = (tos)
//设置数据报总长度
#define IPH_LEN_SET(hdr, len) (hdr)->_len = (len)
//设置标识
#define IPH_ID_SET(hdr, id) (hdr)->_id = (id)
//设置分片标志与偏移量
#define IPH_OFFSET_SET(hdr, off) (hdr)->_offset = (off)
//设置生存时间
#define IPH_TTL_SET(hdr, ttl) (hdr)->_ttl = (u8_t)(ttl)
//设置上层协议
#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto = (u8_t)(proto)
//设置校验和
#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum = (chksum)

IP数据报分片

从IP首部我们就知道IP数据报分片这个概念,也知道不是每个底层网卡都能承载每个IP数据报长度的报文,例如以太网帧最大能承载1500个字节的数据,而某些广域网链路的帧可承载576字节的数据。一个链路层帧能承载的最大数据量叫做最大传送单元(Maximum Transmission Unit,MTU)。因为每个IP数据报都必须封装在链路层帧中从一台路由器传输到下一台路由器,故链路层协议的MTU严格地限制着IP数据报的长度。对IP数据报长度具有严格限制并不是主要问题,问题在于在发送方与目的地路径上的每段链路可能使用不同的链路层协议,且不同的硬件可能具有不同的MTU,这就需要有一个很好的处理方式,随之而来的就是IP数据报分片处理。

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

所有分片数据报在其到达目标主机的传输层之前需要在IP层完成重新组装(也称之为重装)。IPv4协议的设计者觉得如果在每个IP层中组装分片数据包,那么将严重影响路由器的性能,例如一台路由器,在收到数据分片后又进行重装,然后再转发,这样子的处理是万万不可的,所以 IPv4的设计者决定将数据报的重新组装工作放到端系统中,而不是放到网络路由器中,什么是端系统呢?简单来说就是数据包中的目标IP地址的主机,在这台机器上的IP层进行数据分片的重装,这样子数据分片可以任意在各个路由之间进行转发,而路由器就无须理会数据分片是在哪里重装,只要数据分片不是给路由器的,那么就将其转发出去即可,当然,这样子的处理就会是的每个数据分片到达目标IP地址的主机时间是不一样的。

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

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

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

ip分片

IP数据报分片源码实现

在LwIP中IP数据报分片是通过ip4_frag()函数实现的,整个函数是比较复杂的,主要是循环处理数据报的分片,主要是处理偏移量与分片标志,复制原始数据的部分到分片空间中并发送出去,然后填写IP数据报首部的其他字段,如果是分片的最后一个数据报,则修改标志位并且发送出去,发送完成则释放分片空间。

err_t
ip4_frag(struct pbuf *p,
 struct netif *netif,
 const ip4_addr_t *dest)
{
  struct pbuf *rambuf;
  struct pbuf *newpbuf;
  u16_t newpbuflen = 0;
  u16_t left_to_copy;
  struct ip_hdr *original_iphdr;
  struct ip_hdr *iphdr;
  const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8);
  u16_t left, fragsize;
  u16_t ofo;
  int last;
  u16_t poff = IP_HLEN;
  u16_t tmp;
  int mf_set;

  //原来的数据区域
  original_iphdr = (struct ip_hdr *)p->payload;
  iphdr = original_iphdr;
  if (IPH_HL_BYTES(iphdr) != IP_HLEN) {
    /* 如果ip4_frag不支持IP选项 */
    return ERR_VAL;
  }

  /* 保存原始偏移量 */
  tmp = lwip_ntohs(IPH_OFFSET(iphdr));
  ofo = tmp & IP_OFFMASK;
  /* 得到更多的分配标志位 */
  mf_set = tmp & IP_MF;
  
  /* 得到要发送数据的长度 */
  left = (u16_t)(p->tot_len - IP_HLEN);

  //要发送的数据长度大于0
  while (left) 
  {
    fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));//4000  1480
    
    //申请分片pbuf结构
    rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
    if (rambuf == NULL) {
      goto memerr;
    }
    LWIP_ASSERT("this needs a pbuf in one piece!",
                (rambuf->len >= (IP_HLEN)));
    //拷贝原始数据的部分到分片中
    SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
    
    //得到分片包存储区域
    iphdr = (struct ip_hdr *)rambuf->payload;

    //更新还需要拷贝的数据
    left_to_copy = fragsize;
    
    while (left_to_copy) 
    {
      struct pbuf_custom_ref *pcr;
      
      //定义记录已经拷贝的数据大小变量 plen
      u16_t plen = (u16_t)(p->len - poff);
      
      //需要创建一个新pbuf拷贝剩下的
      newpbuflen = LWIP_MIN(left_to_copy, plen);
      if (!newpbuflen) 
      {
        poff = 0;
        p = p->next;
        continue;
      }
      //申请分片新的pbuf
      pcr = ip_frag_alloc_pbuf_custom_ref();
      if (pcr == NULL) 
      {
        pbuf_free(rambuf);
        goto memerr;
      }
      /* 初始化这个pbuf */
      newpbuf = pbuf_alloced_custom(PBUF_RAW, 
                                    newpbuflen, 
                                    PBUF_REF, 
                                    &pcr->pc,
                                    (u8_t *)p->payload + poff, 
                                    newpbuflen);
      
      if (newpbuf == NULL) 
      {
        ip_frag_free_pbuf_custom_ref(pcr);
        pbuf_free(rambuf);
        goto memerr;
      }
      
      pbuf_ref(p);
      pcr->original = p;
      pcr->pc.custom_free_function = ipfrag_free_pbuf_custom;

      //将它添加到rambuf链的末尾
      pbuf_cat(rambuf, newpbuf);
      left_to_copy = (u16_t)(left_to_copy - newpbuflen);
      if (left_to_copy) 
      {
        poff = 0;
        p = p->next;
      }
    }
    
    //更新数据报的偏移量
    poff = (u16_t)(poff + newpbuflen);

    /* 处理分片 */
    last = (left <= netif->mtu - IP_HLEN);

    /* 设置新的偏移和MF标志 */
    tmp = (IP_OFFMASK & (ofo));
    if (!last || mf_set) 
    {
      tmp = tmp | IP_MF;
    }
    //填写分片相关字段
    IPH_OFFSET_SET(iphdr, lwip_htons(tmp));
    IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + 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 = (u16_t)(left - fragsize);
    //分片偏移增加
    ofo = (u16_t)(ofo + nfb);
  }
  MIB2_STATS_INC(mib2.ipfragoks);
  return ERR_OK;
memerr:
  MIB2_STATS_INC(mib2.ipfragfails);
  return ERR_MEM;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值