lwip源码分析 之 IP协议 数据输入 (二)

一 导读

在上一章 ip数据输入中,输入的ip分组会被判断是否是一个被分片的分组。若是一个分片的ip数据,则需要将分片暂存起来,等接收完所有的分片再将分片重装成一个完整的ip数据传递给传输层。

这章就介绍lwip如何实现分片的重装。

二,重装数据结构

由于ip分组在网络传输过程中到达目的地点的时间是不确定的,所以后面的分组可能比前面的分组先达到目的地点。

如图所示,分片A,B,C代表一个完整的ip数据报,他们的目的地址都是192.168.1.1,片偏移的单位是字(32bit),总长度的单位是字节,最后MF标志位表示其后是否有数据。

A分片的片偏移为0,说明它是ip数据的第一个分片,数据长度为1420-20=1400.(20字节ip首部)。B分片片偏移为 1400/8=175;C分片MF为0,说明它是最后一个分片。注意ABC 三分片到达目的站的时间是不确定的。

在这里插入图片描述
图中只为方便表示,具体ip首部不是图中所示的位置。

为此,我们需要将接收到的分组先暂存起来,等所有的分组都接收完成,再将数据传递给上层。在lwip中,有专门的结构体负责缓存这些分组。

结合示意图理解结构体成员的意思。

//重装数据结构体
struct ip_reassdata {
    
    
  struct ip_reassdata *next;
  struct pbuf *p; //ip数据报pbuf链
  struct ip_hdr iphdr;  //ip数据报首部(即第一个分组的ip首部)
  u16_t datagram_len; //完整ip数据报大小
  u8_t flags; //标志是否最后一个分组
  u8_t timer; //超时间隔
};

在lwip中会维持一条ip_reassdata链,每一个ip_reassdata结构体代表一个正在重装的ip数据报。当接收到分组时,会将分组的pbuf连接到对应ip_reassdata里的pbuf链中。当该ip数据报接收完整后才递交上层。
在这里插入图片描述
在上图中,ip分片首部的8个字节的数据被修改成一个结构体ip_reass_helper,该结构体是用于连接pbuf与判断数据是否完整。定义如下(后面详细分析):

PACK_STRUCT_BEGIN
struct ip_reass_helper {
    
    
  PACK_STRUCT_FIELD(struct pbuf *next_pbuf);  
  PACK_STRUCT_FIELD(u16_t start); //该分组数据起始序号
  PACK_STRUCT_FIELD(u16_t end); //数据结束序号
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

三,代码分析

1, 分片处理

该函数先检查分片大小是否超出限制,从重装数据报链中找到对应的数据报,将分片插入数据报,根据插入结果决定是否重新组装pbuf成一个完整的ip数据报,递交上层。在理解分片重新组装pbuf时,多结合上面的示意图理解。

//将输入的分组插入对应reassdata的pbuf链,若重装完成则返回完整数据的pbuf
struct pbuf *
ip4_reass(struct pbuf *p)
{
    
    
  struct pbuf *r; 
  struct ip_hdr *fraghdr; //输入分片的首部
  struct ip_reassdata *ipr; //分片对应的重装数据报
  struct ip_reass_helper *iprh; //pbuf中被强制转换的8个字节,用于指向下一个pbuf与分组起始和结束
  u16_t offset, len, clen;  //offset:片偏移;len:分片长度;clen:分片的pbuf数量
  int valid;  //分片插入链表的结果
  int is_last;  //最后一个分片
  
  fraghdr = (struct ip_hdr*)p->payload; //获取分组首部

  //ip首部不正常
  if ((IPH_HL(fraghdr) * 4) != IP_HLEN) {
    
    
    goto nullreturn;
  }
  //得到分组的片偏移量(相对于0的偏移量)
  offset = (lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) * 8;
  len = lwip_ntohs(IPH_LEN(fraghdr)) - IPH_HL(fraghdr) * 4; //获取分组的数据长度

  clen = pbuf_clen(p);  //获取输入分组的pbuf数量
  //若将输入报文的pbuf加上reassdatagrams链表所有的pbuf的数量超出限制
  if ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS) {
    
    
#if IP_REASS_FREE_OLDEST
    //则释放掉最老的reassdatagrams,并再次检查长度是否超出
    if (!ip_reass_remove_oldest_datagram(fraghdr, clen) ||
        ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS))
#endif /* IP_REASS_FREE_OLDEST */
    {
    
    
      //长度还是超出,返回错误
      /* @todo: send ICMP time exceeded here? */
      /* drop this pbuf */
      goto nullreturn;
    }
  }
  //遍历分组重装链表,找到该分组对应的reassdatagrams
  for (ipr = reassdatagrams; ipr != NULL; ipr = ipr->next) {
    
    
    //找到对应的reassdatagrams,退出循环 此时ipr不为空
    if (IP_ADDRESSES_AND_ID_MATCH(&ipr->iphdr, fraghdr)) {
    
    
      break;
    }
  }
  if (ipr == NULL) {
    
    
    //未找到其对应的ip数据报,新建一个数据报
    ipr = ip_reass_enqueue_new_datagram(fraghdr, clen);
    if (ipr == NULL) {
    
    
      goto nullreturn;
    }
  } else {
    
    
    //找到对应的数据报,若输入分组的片偏移为0,则是数据报的第一个分组
    if (((lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) == 0) &&
      ((lwip_ntohs(IPH_OFFSET(&ipr->iphdr)) & IP_OFFMASK) != 0)) {
    
    
      //复制输入分组的ip首部到ipr的iphdr成员,由它的首部作为完整数据报的ip首部
      SMEMCPY(&ipr->iphdr, fraghdr, IP_HLEN); 
    }
  }

  //到此,我们为输入分组找到了对应的数据报
  is_last = (IPH_OFFSET(fraghdr) & PP_NTOHS(IP_MF)) == 0; //是否是最后一个分组.MF=0
  if (is_last) {
    
    
    u16_t datagram_len = (u16_t)(offset + len); //计算数据报长度
    //u16 类型内存溢出
    if ((datagram_len < offset) || (datagram_len > (0xFFFF - IP_HLEN))) {
    
    
     goto nullreturn;
    }
  }
  /* @todo: trim pbufs if fragments are overlapping */
  //将分片插入链表并检查重装是否完成
  valid = ip_reass_chain_frag_into_datagram_and_validate(ipr, p, is_last);

  //插入失败
  if (valid == IP_REASS_VALIDATE_PBUF_DROPPED) {
    
    
    goto nullreturn;  
  }
  //到此,分片的插入完成
  //ip_reass_pbufcount增加clen个pbuf
  ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount + clen);
  if (is_last) {
    
    
    //最后一个分片已经到来
    u16_t datagram_len = (u16_t)(offset + len); //计算数据报长度
    ipr->datagram_len = datagram_len;
    ipr->flags |= IP_REASS_FLAG_LASTFRAG; //设置数据报标志
  }

  //若数据报已完成重装,构建一个存放完整ip数据报的pbuf链(填充首部,连接pbuf),并将ipr从reassdatagrams链表删除
  if (valid == IP_REASS_VALIDATE_TELEGRAM_FINISHED) {
    
    
    struct ip_reassdata *ipr_prev;
    ipr->datagram_len += IP_HLEN; //设置数据报总长度 数据长度+首部长度20字节

    r = ((struct ip_reass_helper*)ipr->p->payload)->next_pbuf;  //保存第二个pbuf(后面有用)

    fraghdr = (struct ip_hdr*)(ipr->p->payload);  //fraghdr现在是第一个pbuf的payload,指向ip数据报的首部(我们需要填充这个首部)
    SMEMCPY(fraghdr, &ipr->iphdr, IP_HLEN); //结合示意图。用之前保存在iphadr中的首部数据填充pbuf首部
    IPH_LEN_SET(fraghdr, lwip_htons(ipr->datagram_len));  //设置数据报的长度
    IPH_OFFSET_SET(fraghdr, 0);   //设置片偏移为0
    IPH_CHKSUM_SET(fraghdr, 0);
    /* @todo: do we need to set/calculate the correct checksum? */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(ip_current_input_netif(), NETIF_CHECKSUM_GEN_IP) {
    
    
      IPH_CHKSUM_SET(fraghdr, inet_chksum(fraghdr, IP_HLEN)); //设置校验和
    }
#endif /* CHECKSUM_GEN_IP */

    p = ipr->p; //p是数据报第一个pbuf

    //将同一数据报中分片的pbuf链连接连接在一起
    while (r != NULL) {
    
    
      iprh = (struct ip_reass_helper*)r->payload; //iprh->next_pbuf指向下一个分片

      pbuf_header(r, -IP_HLEN); //将下一个分片第一个pbuf的payload后移,指向数据区,隐藏掉ip首部(我们只需要一个ip首部)
      pbuf_cat(p, r); //将分片的pbuf链 连接到p上
      r = iprh->next_pbuf;  //获取下一个分片的pbuf链
    }

    //确定ipr_prev,并删除ipr
    if (ipr == reassdatagrams) {
    
    
      ipr_prev = NULL;
    } else {
    
    
      for (ipr_prev = reassdatagrams; ipr_prev != NULL; ipr_prev = ipr_prev->next) {
    
    
        if (ipr_prev->next == ipr) {
    
    
          break;
        }
      }
    }

    ip_reass_dequeue_datagram(ipr, ipr_prev); //从链表中删除ipr

    ip_reass_pbufcount -= pbuf_clen(p); //重装pbuf数量减少

    MIB2_STATS_INC(mib2.ipreasmoks);

    return p;
  }
  /* the datagram is not (yet?) reassembled completely */
  LWIP_DEBUGF(IP_REASS_DEBUG,("ip_reass_pbufcount: %d out\n", ip_reass_pbufcount));
  return NULL;

nullreturn:
  LWIP_DEBUGF(IP_REASS_DEBUG,("ip4_reass: nullreturn\n"));
  IPFRAG_STATS_INC(ip_frag.drop);
  pbuf_free(p);
  return NULL;
}

2,分组插入

上面提到的将分组插入数据报,其实现也是一个重要的函数。该函数将分片插入链表,并检查链表中的数据是否完整,若数据完整则返回1,在ip4_reass()中就会将完整的数据交给上层。

其中valid值标志数据是否完整。valid初始为1,在分组插入链表的过程中,通过检查分组的数据之间是否连续来修改valid,若有一处的分组不连续,那么数据报肯定不完整,valid为0,只有遍历完链表后valid仍为1且最后一个分组收到时,才说明数据报完整。

在这个函数中,要注意ip_reass_helper这个结构体的作用。该结构体的start和end是判断数据是否连续的关键。

//将分片插入分组链
static int
ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last)
{
    
    
  struct ip_reass_helper *iprh, *iprh_tmp, *iprh_prev=NULL;
  struct pbuf *q;
  u16_t offset, len;  //offset:分片片偏移;len:分片数据长度
  struct ip_hdr *fraghdr;
  int valid = 1;  //数据连续标志。初始化valid为1,当出现前后分组数据不连续时,valid为0,说明数据报不完整

  fraghdr = (struct ip_hdr*)new_p->payload; //获取当前分片的首部
  len = lwip_ntohs(IPH_LEN(fraghdr)) - IPH_HL(fraghdr) * 4; //计算len:分片的数据长度
  offset = (lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) * 8;  //计算分片的片偏移

  iprh = (struct ip_reass_helper*)new_p->payload; //将分片首部的8个字节的数据转换成ip_reass_helper
  iprh->next_pbuf = NULL;
  iprh->start = offset; //填充iprh的开始序号
  iprh->end = offset + len; //结束序号

  //遍历分组链表中的分组的第一个pbuf的ip_reass_helper结构体(序号从小到大)
  //并在合适的位置插入new_pbuf
  for (q = ipr->p; q != NULL;) {
    
    
    //分组的ip_reass_helper,重要!用于确定分组插入位置
    iprh_tmp = (struct ip_reass_helper*)q->payload; 
    if (iprh->start < iprh_tmp->start) {
    
     
      //输入分组start小于当前分组,可插入
      iprh->next_pbuf = q;
      if (iprh_prev != NULL) {
    
     
        //前面的pbuf与当前的pbuf数据内容出现覆盖
        if ((iprh->start < iprh_prev->end) || (iprh->end > iprh_tmp->start)) {
    
    
          goto freepbuf;
        }
        iprh_prev->next_pbuf = new_p;

        //前一个pbuf的结尾不等当前pbuf的结尾序号,说明两个pbuf数据不是连续的
        if (iprh_prev->end != iprh->start) {
    
    
          valid = 0;
        }
      } else {
    
      //当前分片偏移最小
        //若当前分片出现覆盖,退出
        if (iprh->end > iprh_tmp->start) {
    
    
          goto freepbuf;
        }
        ipr->p = new_p; //插入链表的最前
      }
      break;
    } else if (iprh->start == iprh_tmp->start) {
    
      //接收到重复的分组
      goto freepbuf;
    } else if (iprh->start < iprh_tmp->end) {
    
       //分片的内容与iprh_temp的内容重叠,退出

      goto freepbuf;
    } else {
    
      //iprh->start > iprh_tmp->end
      //检查数据报是否连续
      if (iprh_prev != NULL) {
    
    
        if (iprh_prev->end != iprh_tmp->start) {
    
    
          valid = 0;  //不连续
        }
      }
    }
    q = iprh_tmp->next_pbuf;  //检查下一个分片
    iprh_prev = iprh_tmp;
  }

  if (q == NULL) {
    
    
    //若q为null,输入分组序号最高 则说明分组应该在链表的最末
    if (iprh_prev != NULL) {
    
    
      iprh_prev->next_pbuf = new_p; //插入链尾
      //判断是否连续
      if (iprh_prev->end != iprh->start) {
    
    
        valid = 0;  
      }
    } else {
    
    
      ipr->p = new_p; //到这里只能是链表为null
    }
  }

  //若最后一个分组已经收到
  if (is_last || ((ipr->flags & IP_REASS_FLAG_LASTFRAG) != 0)) {
    
    
    //且输入分片与它前面的分片的数据连续无断点
    if (valid) {
    
    
      //检查数据报首部是否为null或者偏移是不是0
      if ((ipr->p == NULL) || (((struct ip_reass_helper*)ipr->p->payload)->start != 0)) {
    
    
        valid = 0;  //以上情况说不对
      } else {
    
    
        //检查输入分组之后的分组是否也是连续的
        iprh_prev = iprh; 
        q = iprh->next_pbuf;  //从输入分组之后开始遍历检查
        while (q != NULL) {
    
    
          iprh = (struct ip_reass_helper*)q->payload;
          if (iprh_prev->end != iprh->start) {
    
    
            //出现不连续的数据
            valid = 0;  
            break;  //跳出循环。不用再检查了。
          }
          iprh_prev = iprh;
          q = iprh->next_pbuf;
        }

        if (valid) {
    
    
          LWIP_ASSERT("sanity check", ipr->p != NULL);
          LWIP_ASSERT("sanity check",
            ((struct ip_reass_helper*)ipr->p->payload) != iprh);
          LWIP_ASSERT("validate_datagram:next_pbuf!=NULL",
            iprh->next_pbuf == NULL);
        }
      }
    }
    //返回1说明数据报重装完成
    return valid ? IP_REASS_VALIDATE_TELEGRAM_FINISHED : IP_REASS_VALIDATE_PBUF_QUEUED;
  }
  /* If we come here, not all fragments were received, yet! */
  return IP_REASS_VALIDATE_PBUF_QUEUED; /* not yet valid! */
#if IP_REASS_CHECK_OVERLAP
freepbuf:
  ip_reass_pbufcount -= pbuf_clen(new_p);
  pbuf_free(new_p);
  return IP_REASS_VALIDATE_PBUF_DROPPED;
#endif /* IP_REASS_CHECK_OVERLAP */
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44821644/article/details/111655386