【lwip】09-IPv4协议&超全源码实现分析

乎语百科 306 0
目录

前言

默认主讲ipv4。

概念性的内容简单过一遍即可,主要还是在源码实现方面。

原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16859723.html

9.1 IP协议简述

IP 协议(Internet Protocol),又称之为网际协议,IP 协议处于 IP 层工作,它是整个 TCP/IP 协议栈的核心协议,上层协议都要依赖 IP 协议提供的服务,IP 协议负责将数据报从源主机发送到目标主机,通过 IP 地址作为唯一识别码。

IP 协议是一种无连接的不可靠数据报交付协议,协议本身不提供任何的错误检查与恢复机制。

IP地址是协议地址,MAC地址是硬件地址,所处的层级不一样。

常见的广域网路由器就工作在IP层。

在本次笔记lwip源码实现分析内容概述:

  • IP地址及其分类、特殊IP地址;
  • 子网划分、子网掩码、NAT等概念;
  • IP层数据包结构一级数据报输入处理;
  • IP层数据包的发送及分片操作;
  • IP层分片数据重装过程。

9.2 IP地址分类

【lwip】09-IPv4协议&超全源码实现分析

A类地址:1.0.0.1—126.155.255.254

B类地址:128.0.0.1—191.255.255.254

C类地址:192.0.0.1—223.255.255.254

D类地址:224.0.0.1—239.255.255.254

E类地址:240.0.0.1—255.255.255.254

具体更加细致的地址作用,自行百度。

9.2.1 私有地址

A类的:10.x.x.x 是私有地址。

B类的:172.16.0.0—172.31.255.255 是私有地址。

C类的:全都是。192.0.0.1—223.255.255.254 是私有地址。

9.2.2 受限广播地址

受限广播地址是网络号与主机号都为 1 的地址:255.255.255.255

为了避免这个广播地址往整个互联网里发送广播包,在任何情况下,路由器都会禁止转发目的地址为 255.255.255.255 的广播数据包,要不然会给整个互联网带来网络性灾难。

9.2.3 直接广播地址

直接广播地址是主机号全为 1 而得到的地址,广播地址代表本网络内的所有主机,使用该地址可以向网络内的所有主机发送数据。

A 类地址的广播地址为:XXX.255.255.255

B 类地址的广播地址为:XXX.XXX.255.255

C 类地址的广播地址为:XXX.XXX.XXX.255

9.2.4 多播地址

多播地址属于分类编址中的 D 类地址,D 类地址只能用作目的地址,而不能作为主机中的源地址。

多播地址用在一对多的通信。

9.2.5 环回地址

127.x.x.x 是保留地址,用作循环测试用(127.0.0.1 为保留地址,一般用于环回地址)

9.2.6 本地链路地址

169.254.x.x 是本地链路地址。

AUTOIP协议使用。

即是如果 IP 地址是自动获取 IP 地址,而在网络上又没有找到可用的 DHCP 服务器,就会得到其中一个 IP。

9.2.7 本网络本主机地址

IP 地址 32bit 全为 0 的地址(0.0.0.0)表示的是本网络本主机,只能做源IP地址。

在当设备启动时但又不知道自己的 IP 地址情况下常见。

9.2.8 子网

子网掩码 & 判断是否在同一子网: IP 与 子网掩码 做 按位与 ,就可以得出该 IP 的子网网段。 如:

  • 子网掩码:255.255.255.0
  • IP-1: 192.168.1.2 & 255.255.255.0 = 192.168.1.0
  • IP-2: 192.168.1.123 & 255.255.255.0 = 192.168.1.0
  • IP-3: 192.168.2.123 & 255.255.255.0 = 192.168.2.0
  • 因为 192.168.1.0 = 192.168.1.0,所以IP-1与IP-2处于同一子网。
  • 因为 192.168.1.0 != 192.168.2.0,所以IP-1与IP-3不在同一子网。

在发数据包时,子网的作用

  • 若源IP和目标IP在同一子网:直接获取目标IP主机的MAC,然后把数据包丢出去。
  • 若源IP和目标IP不在同一子网:获取默认网关的 MAC ,然后把数据包丢给默认网关那边。

9.2.9 NAT 概念

在计算机网络中,网络地址转换(Network Address Translation,缩写为 NAT),也叫做网络掩蔽或者 IP 掩蔽(IP masquerading)。

当前只需要知道NAT就是网络地址转换的作用即可,详细技术细节自行百度。

9.3 IP数据报

【lwip】09-IPv4协议&超全源码实现分析

9.3.1 版本号字段

占用4 bit。

这个字段规定了数据报的 IP 协议版本。

如:

  • IPv4:值为4。
  • IPv6:值为6。

9.3.2 首部长度字段

占用4 bit。

用于记录 IP 首部的数据的长度。

单位,字。最大可以表示15*4=60字节。

IP首部的长度默认是20 byte,但是如果有选项字段,就不止20 byte了。

9.3.3 服务类型(TOS:type of service)字段

占用8 bit。

该字段用于描述当前IP数据报急需的服务类型,如:

  • 最小延时;
  • 最大吞吐量;
  • 最高可靠性;
  • 最小费用等。

路由器在转发数据报时,可以根据该字段的值来为数据报选择最合理的路由路径。

9.3.4 总长度字段

占用16 bit。

是 IP 数据报的总长度(IP首部+数据区)。

单位,字节。最大能表示65535字节。

数据报很少有超过1500字节的,因为以太网数据帧的数据最大长度为1500字节。

如果一个IP数据报过大时,需要进行分片处理。

9.3.5 标识字段

标识字段、标志字段和13位偏移字段常在IP数据报分片时使用。

占用16 bit。

用于标识IP层发出去的每一份IP数据报,每发送一份,该值+1。

如果IP数据报被分片,该字段在每个分片的IP数据报上是一致的,表示属于同一个IP数据报。

在接收端会根据该字段识别同一个IP数据报进行重装。

9.3.6 标志字段

占用3 bit。

第一个bit保留未用。

第二个bit是不分片标志位

  • 0:则表示 IP 层在必要的时候可以对其进行分片处理。
  • 1:则表示 IP 数据报在发送的过程中不允许进行分片,如果这个 IP 数据报的大小超过链路层能承载的大小,这个 IP 数据报将被丢弃。

第三个bit是更多分片标志位

  • 0:后续没有更多分片。即是当前分片的IP数据报是最后一个分片。
  • 1:表示后续还有分片。即是当前分片的IP数据报不是最后一个分片。

9.3.7 分片偏移量字段

占用13 bit。

表示当前分片所携带的数据在整个 IP 数据报中的相对偏移位置。

单位,8字节(2个字)。

目标主机要接收到从0分片偏移量到最高分片偏移量的所有分片才能进行组装出完整的IP数据报。

9.3.8 生存时间(Time-To-Live,TTL)字段

占用8 bit。

该字段用来确保数据报不会永远在网络中循环。

IP数据报没经过一台路由器处理,该值-1。

如果TTL字段下降到0,则路由器会丢弃该数据报,且会返回一个 ICMP 差错报文给源主机。

9.3.9 协议字段

占用8 bit。

表示上层协议类型。即是表示数据区的数据是哪个协议的数据报。如:

  • 6:表示TCP协议。
  • 17:表示UDP协议。
  • 其它值可以自行度娘。

9.3.10 首部校验和字段

占用16 bit。

只针对IP首部做校验,并不关系数据区在传输过程中是否出错,所以对于数据区的校验需要由上层协议负责。

路由器要对每个收到的 IP 数据报计算其首部检验和,如果数据报首部中携带的检验和与计算得到的检验和不一致,则表示出现错误,路由器一般会丢弃检测出错误的 IP 数据报。

需要注意的是,由于IP数据报首部的TTL字段每结果应该路由器都会-1,所以IP数据报首部检验和字段每经过一个路由器都要重新计算赋值。

参考:关于wireshark的Header checksum出问题解决方案:https://www.it610.com/article/1290714377560858624.htm

检验和计算可能由网络网络驱动,协议驱动,甚至是硬件完成。

高层校验通常是由协议执行,并将完成后的包转交给硬件。

比较新的网络硬件可以执行一些高级功能,如IP检验和计算,这被成为checksum offloading。网络驱动不会计算校验和,只是简单将校验和字段留空或填入无效信息,交给硬件计算。

发送数据时首部校验和计算:二进制反码求和。

  • 把IP数据包的校验和字段置为全0。
  • 将首部中的每 2 个字节当作一个数,依次求和。
  • 把结果取反码。
  • 把得到的结果存入校验和字段中。

接收数据时,首部校验和验证过程:

  • 首部中的每 2 个字节当作一个数,依次进行求和,包括校验和字段。
  • 检查计算出的校验和的结果是否全为1(反码应为16个0)。
  • 如果等于零,说明被整除,校验和正确。否则,校验和就是错误的,协议栈要抛弃这个数据包。

为什么计算出的校验和结果全为1?

因为如果校验依时次求和,不包含校验和字段的话,得出的值就是校验和字段的反码。

校验和的反码和校验和求和,当然是全1啦。

9.3.11 二进制反码求和

IP/ICMP/IGMP/TCP/UDP等协议的校验和算法都是相同的。

二进制反码求和:(求和再反码,结果一致)

  • 对两个二进制数进行加法运算。
  • 加法规则:0+0=0,0+1=1,1+1=10(进位1加到下一bit)。
  • 若最高两位相加仍然有进位,则在最后的结果中+1即可。
  • 对最终结果取反码。

相关源码参考LwIP\core\inet_chksum.c中的lwip_standard_chksum()

这里提取版本3分析:

  • 前期先确保4字节对齐,如果不是4字节对齐,就补到4字节对齐。

  • 后面采用32 bit累加。溢出后,在低位+1。

    • 为什么?:这里读者可能会有个疑问,IP数据包的校验和不是要求16 bit求和的吗?这里为什么能用32 bit求和?
    • 答:起始要求是16 bit,但是实际计算时只要大于16 bit即可,因为到最后,可以把高位折叠加到低位。
    • 例子:按32bit累加,溢出就在低位+1。其实就是两组两个(高、低)16 bit对应累加,低16 bit累加的进位给高16 bit里加回1了。而高16 bit累加的进位在底16 bit里加回1了(手动)。这样,累加到最后剩下32bit。把高16bit和低16bit进行累加,进位再加1即可快速得到16bit的校验和。
  • 数据后部分可能不是8字节对齐,所以剩余的字节也需要16bit校验和处理。

思路图:

【lwip】09-IPv4协议&超全源码实现分析

由于目的是16 bit的校验和。其实可以看成两组2个8bit对应相加,低8bit组进位给高8bit组,高8bit组进位给低8bit组。所以相加值是对应高、低8bit相互独立的。

而下面函数就是利用这个特性,如果首字节为奇地址,先单独取出来放到t的高地址,因为后续的统计字节顺序是返的。等待全部统计完毕后,再把两个字节顺序调换即可。

如果是偶地址开始,那符合校验和规则,最后不需要调换字节顺序。

#if (LWIP_CHKSUM_ALGORITHM == 3) /* Alternative version #3 */
/**
 * An optimized checksum routine. Basically, it uses loop-unrolling on
 * the checksum loop, treating the head and tail bytes specially, whereas
 * the inner loop acts on 8 bytes at a time.
 *
 * @arg start of buffer to be checksummed. May be an odd byte address.
 * @len number of bytes in the buffer to be checksummed.
 * @return host order (!) lwip checksum (non-inverted Internet sum)
 *
 * by Curt McDowell, Broadcom Corp. December 8th, 2005
 */
u16_t
lwip_standard_chksum(const void *dataptr, int len)
{
  const u8_t *pb = (const u8_t *)dataptr; /* 取数据的地址 */
  const u16_t *ps;
  u16_t t = 0;
  const u32_t *pl;
  u32_t sum = 0, tmp;

  int odd = ((mem_ptr_t)pb & 1); /* 判断是否为奇地址 */

  if (odd && len > 0) { /* 如果不是2直接对齐 */
    /* 缓存奇地址上的字节,存于 t 的高位。数据地址偏移为偶,2字节对齐。 */
    /* 存到高位是为了和后面字节序保持一致,方便在最后一次性更正。 */
    ((u8_t *)&t)[1] = *pb++;
    len--; /* 字节数-1 */
  }

  /* 2字节对齐的数据起始地址 */
  ps = (const u16_t *)(const void *)pb;

  if (((mem_ptr_t)ps & 3) && len > 1) {/* 如果不是4字节对齐 */
    /* 把多出来的两字节保存到sum */
    sum += *ps++;
    len -= 2;
  }

  /* 4字节对齐的数据起始地址 */
  pl = (const u32_t *)(const void *)ps;

  while (len > 7)  {
    tmp = sum + *pl++;          /* ping */
    if (tmp < sum) {
      tmp++;                    /* 溢出,手动+1 */
    }

    sum = tmp + *pl++;          /* pong */
    if (sum < tmp) {
      sum++;                    /* 溢出,手动+1 */
    }

    len -= 8;
  }

  /* 折叠高、低16bit */
  sum = FOLD_U32T(sum);

  /* 处理剩余的字节 */
  ps = (const u16_t *)pl;

  /* 2字节处理 */
  while (len > 1) {
    sum += *ps++;
    len -= 2;
  }

  /* 剩余单字节 */
  if (len > 0) {                /* 补到前面t的低位 */
    ((u8_t *)&t)[0] = *(const u8_t *)ps;
  }

  sum += t;                     /* 把t也一起16bit校验和 */

  /* 两次折叠高、低16bit */
  sum = FOLD_U32T(sum);
  sum = FOLD_U32T(sum);

  if (odd) { /* 因为前面是从第二个字节和第三个字节开始进行统计的,字节序反了,这里在结果更正。 */
    sum = SWAP_BYTES_IN_WORD(sum);
  }

  return (u16_t)sum; /* 返回校验和 */
}
#endif

9.3.12 源IP字段

占用32 bit。

为源主机的IP地址。

9.3.13 目标IP字段

占用32 bit。

为目标主机的IP地址。

9.3.14 选项字段

0到40字节。

对于IP数据报首部来说,其大小必须为4字节的整数倍。

如果选项字段长度不为4的倍数,则需要用0进行填充。

在 LwIP 中只识别选项字段,不会处理选项字段的内容。

该字段在IPv6报文中已经被移除了。

9.3.15 数据区字段

IP 数据报的最后的一个字段,装载着当前IP数据报的数据,是上层协议的数据报。

9.3.16 对应wireshark包分析

【lwip】09-IPv4协议&超全源码实现分析

9.4 IP首部数据结构

注意:网络字节序是大端的。

ipv4的IP首部数据结构:对应IP首部报文图。

/* The IPv4 header */
struct ip_hdr {
  /* version / header length */
  PACK_STRUCT_FLD_8(u8_t _v_hl); /* 版本号字段(4)+首部长度字段(4)单位字 */
  /* type of service */
  PACK_STRUCT_FLD_8(u8_t _tos); /* 服务类型TOS字段(8) */
  /* total length */
  PACK_STRUCT_FIELD(u16_t _len); /* 总长度字段(16)单位字节 */
  /* identification */
  PACK_STRUCT_FIELD(u16_t _id); /* 标识字段字段(16) */
  /* fragment offset field */
  PACK_STRUCT_FIELD(u16_t _offset); /* 标志字段(3)+分片偏移量字段(13)单位两字 */
#define IP_RF 0x8000U        /* 标志字段第一位:字段保留 */
#define IP_DF 0x4000U        /* 标志字段第二位:不发片掩码 */
#define IP_MF 0x2000U        /* 标志字段第三位:更多分片掩码 */
#define IP_OFFMASK 0x1fffU   /* 分片偏移量字段的掩码 */
  /* time to live */
  PACK_STRUCT_FLD_8(u8_t _ttl); /* TTL字段(8) */
  /* protocol*/
  PACK_STRUCT_FLD_8(u8_t _proto); /* 上层协议类型字段(8) */
  /* checksum */
  PACK_STRUCT_FIELD(u16_t _chksum); /* 首部校验和字段(16) */
  /* source and destination IP addresses */
  PACK_STRUCT_FLD_S(ip4_addr_p_t src); /* 源IP字段(32) */
  PACK_STRUCT_FLD_S(ip4_addr_p_t dest); /* 目的IP字段(32) */
} PACK_STRUCT_STRUCT;

由于IP首部部分字段的操作涉及到bit,所以lwip也封装出对应的宏操作。

/* 均为网络字节序 */
/* Macros to get struct ip_hdr fields: */
#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) /* 获取服务类型TOS */
#define IPH_LEN(hdr) ((hdr)->_len) /* 获取IP数据报总长度 */
#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) /* 获取TTL */
#define IPH_PROTO(hdr) ((hdr)->_proto) /* 获取协议类型 */
#define IPH_CHKSUM(hdr) ((hdr)->_chksum) /* 获取首部校验和字段 */

/* Macros to set struct ip_hdr fields: */
#define IPH_VHL_SET(hdr, v, hl) (hdr)->_v_hl = (u8_t)((((v) << 4) | (hl))) /* 设置版本号 */
#define IPH_TOS_SET(hdr, tos) (hdr)->_tos = (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) /* 设置TTL */
#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto = (u8_t)(proto) /* 设置协议类型 */
#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum = (chksum) /* 设置首部校验和 */

9.5 网卡路由

9.5.1 路由网卡匹配

从官方源码看,匹配网卡的流程:

ip4_route_src()

  • 先函数LWIP_HOOK_IP4_ROUTE_SRC()匹配。
  • 然后到ip4_route()基函数匹配。

9.5.2 路由网卡匹配基函数

ip4_route()

  • 多播IP优先匹配多播专用网卡ip4_default_multicast_netif

  • 遍历网卡:

    • 有效网卡先匹配子网;
    • 子网匹配失败就匹配网关,网卡不能有广播能力。
  • 环回IP:如果开启了各个网卡的环回功能,且没有创建环回网卡:

    • 说明:因为创建了环回网卡,在遍历链表时,就会把环回IP 127.x.x.x都会匹配到环回网卡。
    • 对于环回IP,优先匹配默认网卡netif_default
    • 再遍历网卡,第一个协议栈有效的网卡即可。
  • 钩子匹配:(由用户实现)

    • LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
    • LWIP_HOOK_IP4_ROUTE(dest);
  • 以上都没有匹配成功,则使用netif_default,必须条件:

    • 默认网卡netif_default存在;
    • 默认网卡协议栈有效;
    • 默认网卡数据链路有效;
    • 默认网卡IP有效。
    • 匹配的目的IP不能为环回IP(因为如果是环回IP,前面已经匹配过了,除非没有开启该功能)
/**
 * Finds the appropriate network interface for a given IP address. It
 * searches the list of network interfaces linearly. A match is found
 * if the masked IP address of the network interface equals the masked
 * IP address given to the function.
 *
 * @param dest the destination IP address for which to find the route
 * @return the netif on which to send to reach dest
 */
struct netif *
ip4_route(const ip4_addr_t *dest)
{
#if !LWIP_SINGLE_NETIF
  struct netif *netif;

  /* 确保在tcpip内核锁内 */
  LWIP_ASSERT_CORE_LOCKED();

#if LWIP_MULTICAST_TX_OPTIONS /* 开启了组播TX功能 */
  /* 如果目的IP是一个组播地址,且协议栈内创建了组播专用网卡,则选用组播专用网卡接口 */
  if (ip4_addr_ismulticast(dest) && ip4_default_multicast_netif) {
    return ip4_default_multicast_netif;
  }
#endif /* LWIP_MULTICAST_TX_OPTIONS */

  /* bug #54569: in case LWIP_SINGLE_NETIF=1 and LWIP_DEBUGF() disabled, the following loop is optimized away */
  LWIP_UNUSED_ARG(dest);

  /* 遍历网卡链表 */
  NETIF_FOREACH(netif) {
    /* 网卡协议栈有效,网卡链路层也有效,网卡的IP地址不为全0 */
    if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) {
      /* 匹配传入的目的IP和网卡是否处于同一个子网 */
      if (ip4_addr_net_eq(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) {
        /* 匹配成功。返回netif,以转发IP数据包 */
        return netif;
      }
      /* 当前网卡子网匹配不成功 */
      /* 那就匹配下网关,匹配规则:网卡没有广播功能,目的IP和网关IP一致。也算匹配成功,因为上层的目的是到网关。当前网卡能把数据传达到网卡。 */
      if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_eq(dest, netif_ip4_gw(netif))) {
        /* 匹配成功。返回netif,以转发IP数据包 */
        return netif;
      }
    }
  }

  /* 到这,遍历网卡,匹配失败 */

/* 开启了环回功能,但是没有创建环回网卡。(因为如果创建了环回网卡,在遍历网卡链表时就已经处理过了,这里不需要再处理) */
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF
  /* 如果目的IP是一个环回IP地址,则优先返回默认网卡,否则返回网卡链表中第一个协议栈使能了的网卡作为当前环回网卡 */
  if (ip4_addr_isloopback(dest)) { /* 目的IP是一个环回IP */
    if ((netif_default != NULL) && netif_is_up(netif_default)) {
      /* 优先考虑默认网卡。如果有默认网卡,且默认网卡协议栈使能了,则以此网卡作为本次环回网卡 */
      return netif_default;
    }
    /* 默认网卡没有匹配成功,则从网卡链表中找一个协议栈已使能的网卡作为本次环回网卡 */
    NETIF_FOREACH(netif) {
      if (netif_is_up(netif)) {
        return netif;
      }
    }
    return NULL; /* 都没找到,那就匹配失败 */
  }
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */

#ifdef LWIP_HOOK_IP4_ROUTE_SRC /* 如果开启了匹配网卡的钩子宏函数 */
  netif = LWIP_HOOK_IP4_ROUTE_SRC(NULL, dest); /* 使用宏钩子匹配 */
  if (netif != NULL) {
    return netif;
  }
#elif defined(LWIP_HOOK_IP4_ROUTE) /* 网卡匹配钩子 */
  netif = LWIP_HOOK_IP4_ROUTE(dest); /* 使用宏钩子匹配 */
  if (netif != NULL) {
    return netif;
  }
#endif
#endif /* !LWIP_SINGLE_NETIF */

  /* 上述方案都无法匹配到网卡,就检查网卡网卡是否正常,正常则就返回默认网卡 */

  if ((netif_default == NULL) || !netif_is_up(netif_default) || !netif_is_link_up(netif_default) ||
      ip4_addr_isany_val(*netif_ip4_addr(netif_default)) || ip4_addr_isloopback(dest)) {
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_route: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
    IP_STATS_INC(ip.rterr);
    MIB2_STATS_INC(mib2.ipoutnoroutes);
    return NULL;
  }

  return netif_default; /* 返回默认网络接口 */
}

9.5.3 路由网卡匹配支持源IP和目的IP网卡匹配的接口

匹配网卡,一般是按照目的IP来匹配,但是可以通过LWIP_HOOK_IP4_ROUTE_SRC()钩子宏函数来实现源IP地址和目的IP地址匹配。

ip4_route_src()

  • 如果源IP地址不为空,则会先传入LWIP_HOOK_IP4_ROUTE_SRC()钩子函数来匹配网卡。
  • 钩子函数匹配失败或者源IP地址为空,则由ip4_route()只根据目的IP地址匹配。
#ifdef LWIP_HOOK_IP4_ROUTE_SRC
/**
 * Source based IPv4 routing must be fully implemented in
 * LWIP_HOOK_IP4_ROUTE_SRC(). This function only provides the parameters.
 */
struct netif *
ip4_route_src(const ip4_addr_t *src, const ip4_addr_t *dest)
{
  if (src != NULL) {
    /* 当src==NULL时,钩子会从ip4_route(dest)调用 */
    struct netif *netif = LWIP_HOOK_IP4_ROUTE_SRC(src, dest);
    if (netif != NULL) {
      return netif;
    }
  }
  return ip4_route(dest);
}
#endif /* LWIP_HOOK_IP4_ROUTE_SRC */

9.5.4 路由网卡匹配的钩子函数

通过分析前面的基函数和接口函数,可发现其实现是支持宏钩子函数,即是支持用户自己实现网卡匹配的逻辑的。

有两个宏钩子:

  • LWIP_HOOK_IP4_ROUTE_SRC(src, dest):钩子入口参数有源IP和目的IP。
  • LWIP_HOOK_IP4_ROUTE(dest):钩子入口参数只有目的IP。

9.5.5 收包网卡匹配

当IP层收到一个IP报文时,也要收包网卡匹配。

而且IP包的输入和输出的网卡匹配是不一样的,比如普通的IP单播包,输出时,只需要找到目的IP和网卡处于同一个子网或者是该网卡的网关即可匹配。而输入时,需要明确目的IP就是该网卡IP。

收包的网卡匹配除了ip4_input_accept()这个主要函数外,还有很多独立的匹配条件,具体看IP层输入章节。

这里只分析ip4_input_accept()

  • 在调用该API前,应该先配置全局IP数据结构成员值:struct ip_globals ip_data;

  • 需要被匹配的网卡必须在协议栈方向使能了,且IP地址为有效地址。

    • 单播包,目的地址和网卡地址一致,网卡匹配成功。
    • 广播包,IP地址bit全1,必定是广播地址。如果网卡就被广播能力,且IP地址的主机号bit全1,也是子网广播地址。都匹配成功。
    • 环回,没有环回网卡,且目的IP地址为环回IP IPADDR_LOOPBACK。匹配成功。
/** Return true if the current input packet should be accepted on this netif */
static int
ip4_input_accept(struct netif *netif)
{
  LWIP_DEBUGF(IP_DEBUG, ("ip_input: iphdr->dest 0x%"X32_F" netif->ip_addr 0x%"X32_F" (0x%"X32_F", 0x%"X32_F", 0x%"X32_F")\n",
                         ip4_addr_get_u32(ip4_current_dest_addr()), ip4_addr_get_u32(netif_ip4_addr(netif)),
                         ip4_addr_get_u32(ip4_current_dest_addr()) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
                         ip4_addr_get_u32(netif_ip4_addr(netif)) & ip4_addr_get_u32(netif_ip4_netmask(netif)),
                         ip4_addr_get_u32(ip4_current_dest_addr()) & ~ip4_addr_get_u32(netif_ip4_netmask(netif))));

  /* 网卡是否在协议栈中使能且网卡地址有效? */
  if ((netif_is_up(netif)) && (!ip4_addr_isany_val(*netif_ip4_addr(netif)))) {
    /* 是否是单播到这个接口地址? */
    if (ip4_addr_eq(ip4_current_dest_addr(), netif_ip4_addr(netif)) ||
        /* 或者广播这个接口的网络地址? */
        ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)
#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF /* 如果开启环回功能,但是没有创建环回网卡 */
        /* 目的IP是一个环回的IP地址,也就是给当前网卡的,能匹配成功 */
        || (ip4_addr_get_u32(ip4_current_dest_addr()) == PP_HTONL(IPADDR_LOOPBACK))
#endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */
       ) {
      LWIP_DEBUGF(IP_DEBUG, ("ip4_input: packet accepted on interface %c%c\n",
                             netif->name[0], netif->name[1]));
      /* accept on this netif */
      return 1;
    }
#if LWIP_AUTOIP
    /* 更改netif地址后,链路本地地址的连接必须保持(RFC3927 ch. 1.9) */
    /* 即是netif网卡地址更新为可路由地址了,原本本地链路地址的连接必须保持,所以数据包能得到当前网卡。匹配成功 */
    if (autoip_accept_packet(netif, ip4_current_dest_addr())) {
      LWIP_DEBUGF(IP_DEBUG, ("ip4_input: LLA packet accepted on interface %c%c\n",
                             netif->name[0], netif->name[1]));
      /* accept on this netif */
      return 1;
    }
#endif /* LWIP_AUTOIP */
  }
  return 0;
}

9.6 IP层数据流图

【lwip】09-IPv4协议&超全源码实现分析

9.7 IP层输出

ipv4。

当上层需要发送数据时,会先将自己的数据包组装在一个pbuf中。并将payload指针指向对应协议首部。

然后调用ip_output()发送数据,需要给ip_output()函数提供源IP、目的IP、协议类型、TTL等重要信息让其组IP包。

该函数直接或者间接调用ip4_route_src()根据目的IP选出一个匹配的网卡作为本次IP数据包传输网卡。

选出后调用ip4_output_if()进行IP数据包组包,并调用netif->output()发送出去。或者调用netif_loop_output()环回到本网卡。

9.7.1 发送数据报

上层调用ip_output()把数据转交给IP层处理,lwip支持ipv4和ipv6,这里默认分析ipv4,因为ipv6也是一大块,后面有时间再完整分析下。

/**
 * @ingroup ip
 * Output IP packet, netif is selected by source address
 */
#define ip_output(p, src, dest, ttl, tos, proto) \
        (IP_IS_V6(dest) ? \
        ip6_output(p, ip_2_ip6(src), ip_2_ip6(dest), ttl, tos, proto) : \
        ip4_output(p, ip_2_ip4(src), ip_2_ip4(dest), ttl, tos, proto))

9.7.2 ip层前期处理:ip4_output()

ipv4发包:

  • 检查pbuf的引用ref是否为1。为1才能说明当前pbuf没有被其它地方引用,因为IP层处理可能会改变这个pbuf的部分指针值,如payload。
  • 调用ip4_route_src()匹配网卡。
  • 调用ip4_output_if()把数据包传入IP层处理。
/**
 * Simple interface to ip_output_if. It finds the outgoing network
 * interface and calls upon ip_output_if to do the actual work.
 *
 * @param p the packet to send (p->payload points to the data, e.g. next
            protocol header; if dest == LWIP_IP_HDRINCL, p already includes an
            IP header and p->payload points to that IP header)
 * @param src the source IP address to send from (if src == IP4_ADDR_ANY, the
 *         IP  address of the netif used to send is used as source address)
 * @param dest the destination IP address to send the packet to
 * @param ttl the TTL value to be set in the IP header
 * @param tos the TOS value to be set in the IP header
 * @param proto the PROTOCOL to be set in the IP header
 *
 * @return ERR_RTE if no route is found
 *         see ip_output_if() for more return values
 */
err_t
ip4_output(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
           u8_t ttl, u8_t tos, u8_t proto)
{
  struct netif *netif;

  /* 下传到IP层的pbuf的ref必须为1,即是没有被其它地方引用,因为pbuf下传到IP层后,pbuf的payload指针会被更改。
    如果这个pbuf被其它地方引用了,可能会导致数据混乱。 */
  LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p);

  /* 根据目的IP地址为数据报寻找一个合适的网络接口(匹配网卡) */
  if ((netif = ip4_route_src(src, dest)) == NULL) { /* 没找到,记录信息,返回错误 */
    LWIP_DEBUGF(IP_DEBUG, ("ip4_output: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
                           ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
    IP_STATS_INC(ip.rterr);
    return ERR_RTE;
  }

  /* 匹配到网卡,传入IP层处理:组包、发送 */
  return ip4_output_if(p, src, dest, ttl, tos, proto, netif);
}

9.7.3 发包前的网卡匹配

IP层收到上层的数据包后,需要匹配到网络接口,才能组IP包发出去。

这里调用ip4_route_src()进行网卡匹配。具体分析参考前面。

9.7.4 组建、发送IP包

注意几个函数的区别:

  • ip4_output_if():这个函数封装了底层IP层组包、发送的实现函数。
  • ip4_output_if_src():这个函数就是IP层组包、发送的实现函数。不支持IP首部的选项字段。
  • ip4_output_if_opt():这也是IP层组包、发送的实现函数,会用选中的网卡IP地址覆盖传入的源IP地址。支持IP首部的选项字段。
  • ip4_output_if_opt_src():这也是IP层组包、发送的实现函数,不会用选中的网卡IP地址覆盖传入的源IP地址。支持IP首部的选项字段。

相关宏:

IP_OPTIONS_SEND

  • IP首报文首部选项字段宏开关。
  • 如果开启了该宏,则会调用上述带_opt字样的函数,操作IP首部报文的选项字段。

LWIP_IP_HDRINCL:缺省为NULL

  • 如果把这个宏当目的IP地址传入IP层组包、发送的相关函数ip4_output_if()或其底层函数时,表示当前这个pbuf已经组好IP首部了。
  • 一般用于TCP重传。

LWIP_CHECKSUM_CTRL_PER_NETIF

  • 允许每个网卡配置checksum功能。

相关变量:

  • ip_id:IP首部标识,全局值。

我们就分析ip4_output_if_opt_src()函数,比较全。

ip4_output_if_opt_src()

  • 先处理选项字段,在处理IP首部其它字段。
  • struct pbuf *p:传输层协议需要发送的数据包pbuf,payload指针已指向传输层协议首部。
  • const ip4_addr_t *src:源IP地址。
  • const ip4_addr_t *dest:目的IP地址。
  • u8_t ttl:IP首部TTL字段。
  • u8_t tos:IP首部TOS字段。
  • u8_t proto:IP首部上层协议字段。
  • struct netif *netif:发送IP数据报的网卡。
  • void *ip_options:IP首部选项字段值。
  • u16_t optlen:IP首部选项字段的长度。

概要内容:

  • 通过目的IP判断当前pbuf是否已经组好IP报文首部。如果组好了,就不需要继续重组了。如tcp重传。

  • 如果传入的pbuf报文还没组好IP报文首部,则根据传入的相关数据和IP报文内容进行组包。

  • 组好包后检查目的IP是否是环回IP(如环回IP、当前网卡的IP),如果是就调用netif_loop_output()进行环回处理。

  • 如果不是环回数据包,就需要发到数据链路。

    • IP分片:如果IP报文总长大于网卡MTU,则需要调用ip4_frag()进行IP分片。
    • 如果不需要IP分片,直接调用netif->output()将IP报文发出。
err_t
ip4_output_if_opt_src(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,
                      u8_t ttl, u8_t tos, u8_t proto, struct netif *netif, void *ip_options,
                      u16_t optlen)
{
#endif /* IP_OPTIONS_SEND */
  struct ip_hdr *iphdr;
  ip4_addr_t dest_addr;
#if CHECKSUM_GEN_IP_INLINE
  u32_t chk_sum = 0;
#endif /* CHECKSUM_GEN_IP_INLINE */

  LWIP_ASSERT_CORE_LOCKED(); /* 确保在tcpip内核锁内 */
  LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p); /* pbuf没有被其它地方引用 */

  MIB2_STATS_INC(mib2.ipoutrequests); /* 流量统计 */

  /* IP首部是否已经组建好 */
  if (dest != LWIP_IP_HDRINCL) { /* IP首部未组建好 */
    u16_t ip_hlen = IP_HLEN; /* 默认IP首部长度 */
#if IP_OPTIONS_SEND /* 开启了IP首部选项字段操作 */
    u16_t optlen_aligned = 0;
    if (optlen != 0) {
#if CHECKSUM_GEN_IP_INLINE
      int i;
#endif /* CHECKSUM_GEN_IP_INLINE */
      if (optlen > (IP_HLEN_MAX - IP_HLEN)) { /* 判断IP首部字段是否超限 */
        /* optlen 过长,导致IP首部超出限制 */
        LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: optlen too long\n"));
        /* 状态信息统计 */
        IP_STATS_INC(ip.err);
        MIB2_STATS_INC(mib2.ipoutdiscards);
        return ERR_VAL;
      }
      /* 根据协议要求,IP首部选项字段长度要求是4字节的整数倍 */
      optlen_aligned = (u16_t)((optlen + 3) & ~3); /* 4字节往大对齐 */
      ip_hlen = (u16_t)(ip_hlen + optlen_aligned); /* 总长度字段 */
      /* 先处理选项字段 */
      /* pbuf payload偏移到IP首部选项字段位置 */
      if (pbuf_add_header(p, optlen_aligned)) {
        LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output_if_opt: not enough room for IP options in pbuf\n"));
        /* payload偏移失败,统计信息返回错误 */
        IP_STATS_INC(ip.err);
        MIB2_STATS_INC(mib2.ipoutdiscards);
        return ERR_BUF;
      }
      /* 先处理选项字段,拷贝选项字段值到IP首部中 */
      MEMCPY(p->payload, ip_options, optlen);
      if (optlen < optlen_aligned) {
        /* 多余字节补0 */
        memset(((char *)p->payload) + optlen, 0, (size_t)(optlen_aligned - optlen));
      }
#if CHECKSUM_GEN_IP_INLINE
      /* 先统计这部分的首部校验和 */
      for (i = 0; i < optlen_aligned / 2; i++) {
        chk_sum += ((u16_t *)p->payload)[i];
      }
#endif /* CHECKSUM_GEN_IP_INLINE */
    }
#endif /* IP_OPTIONS_SEND */
    /* 常见IP首部处理 */
    /* pbuf payload指针偏移 */
    if (pbuf_add_header(p, IP_HLEN)) {
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: not enough room for IP header in pbuf\n"));
      /* payload偏移失败,统计信息返回错误 */
      IP_STATS_INC(ip.err);
      MIB2_STATS_INC(mib2.ipoutdiscards);
      return ERR_BUF;
    }

    iphdr = (struct ip_hdr *)p->payload;
    LWIP_ASSERT("check that first pbuf can hold struct ip_hdr",
                (p->len >= sizeof(struct ip_hdr)));

    IPH_TTL_SET(iphdr, ttl); /* 填写TTL字段 */
    IPH_PROTO_SET(iphdr, proto); /* 填写上层协议字段 */
#if CHECKSUM_GEN_IP_INLINE
    chk_sum += PP_NTOHS(proto | (ttl << 8)); /* 首部校验和统计 */
#endif /* CHECKSUM_GEN_IP_INLINE */

    ip4_addr_copy(iphdr->dest, *dest); /* 填写目的IP字段 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校验和统计 */
    chk_sum += ip4_addr_get_u32(&iphdr->dest) & 0xFFFF;
    chk_sum += ip4_addr_get_u32(&iphdr->dest) >> 16;
#endif /* CHECKSUM_GEN_IP_INLINE */

    IPH_VHL_SET(iphdr, 4, ip_hlen / 4); /* 填写IP版本号+IP首部长度 */
    IPH_TOS_SET(iphdr, tos); /* 填写TOS服务字段 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校验和统计 */
    chk_sum += PP_NTOHS(tos | (iphdr->_v_hl << 8));
#endif /* CHECKSUM_GEN_IP_INLINE */
    IPH_LEN_SET(iphdr, lwip_htons(p->tot_len)); /* 填写总长度字段 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校验和统计 */
    chk_sum += iphdr->_len;
#endif /* CHECKSUM_GEN_IP_INLINE */
    IPH_OFFSET_SET(iphdr, 0); /* 填写标志字段+分片偏移量字段 */
    IPH_ID_SET(iphdr, lwip_htons(ip_id)); /* 填写标识字段 */
#if CHECKSUM_GEN_IP_INLINE
    /* 首部校验和统计 */
    chk_sum += iphdr->_id;
#endif /* CHECKSUM_GEN_IP_INLINE */
    ++ip_id; /* ip首部标识,全局值 */

    /* 填写源IP地址字段 */
    if (src == NULL) {
      ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);
    } else {
      /* src cannot be NULL here */
      ip4_addr_copy(iphdr->src, *src);
    }

#if CHECKSUM_GEN_IP_INLINE
    /* 首部校验和统计 */
    chk_sum += ip4_addr_get_u32(&iphdr->src) & 0xFFFF;
    chk_sum += ip4_addr_get_u32(&iphdr->src) >> 16;
    /* 二次16bit折叠。因为一次可能会溢出。第二次是为了解决溢出。 */
    chk_sum = (chk_sum >> 16) + (chk_sum & 0xFFFF);
    chk_sum = (chk_sum >> 16) + chk_sum;
    chk_sum = ~chk_sum; /* 反码 */
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) { /* 当前网卡支持IP首部的checksum功能 */
      iphdr->_chksum = (u16_t)chk_sum; /* 填写首部校验和字段。网络字节序,因为统计的时候就以网络字节序统计,所以这里不必转换 */
    }
#if LWIP_CHECKSUM_CTRL_PER_NETIF
    else {/* 如果不支持checksum功能 */
      IPH_CHKSUM_SET(iphdr, 0); /* 填写首部校验和字段为0 */
    }
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
#else /* CHECKSUM_GEN_IP_INLINE */
    IPH_CHKSUM_SET(iphdr, 0); /* 默认填写首部校验和字段为0 */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
      IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, ip_hlen)); /* IP首部校验和字段更新为实际值 */
    }
#endif /* CHECKSUM_GEN_IP */
#endif /* CHECKSUM_GEN_IP_INLINE */
  } else { /* IP头已经包含在p。如TCP重传 */
    if (p->len < IP_HLEN) { /* 长度校验 */
      LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: LWIP_IP_HDRINCL but pbuf is too short\n"));
      IP_STATS_INC(ip.err);
      MIB2_STATS_INC(mib2.ipoutdiscards);
      return ERR_BUF;
    }
    iphdr = (struct ip_hdr *)p->payload;
    ip4_addr_copy(dest_addr, iphdr->dest); /* 获取目的IP地址 */
    dest = &dest_addr;
  }
  /* 状态记录 */
  IP_STATS_INC(ip.xmit);

  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: %c%c%"U16_F"\n", netif->name[0], netif->name[1], (u16_t)netif->num));
  ip4_debug_print(p);

#if ENABLE_LOOPBACK /* 环回功能 */
  if (ip4_addr_eq(dest, netif_ip4_addr(netif)) /* 目的IP为源网卡IP,则是环回 */
#if !LWIP_HAVE_LOOPIF
      || ip4_addr_isloopback(dest) /* 目的IP是环回IP字段,也是环回 */
#endif /* !LWIP_HAVE_LOOPIF */
     ) {
    /* 数据包环回,则不用通过数据链路层,直达对应网卡的环回链表即可 */
    LWIP_DEBUGF(IP_DEBUG, ("netif_loop_output()"));
    return netif_loop_output(netif, p);
  }
#if LWIP_MULTICAST_TX_OPTIONS
  if ((p->flags & PBUF_FLAG_MCASTLOOP) != 0) { /* 该pbuf是要环回的UDP组播 */
    netif_loop_output(netif, p); /* 环回到本网卡 */
  }
#endif /* LWIP_MULTICAST_TX_OPTIONS */
#endif /* ENABLE_LOOPBACK */
#if IP_FRAG
  /* 如果接口mtu设置为0,不用分片 */
  /* 分片检查 */
  if (netif->mtu && (p->tot_len > netif->mtu)) { /* IP报文超出网卡MTU,则需要分片处理 */
    return ip4_frag(p, netif, dest); /* 需要分片处理 */
  }
#endif /* IP_FRAG */

  /* 不需要分片处理 */
  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));
  return netif->output(netif, p, dest); /* IP层发送数据包。至此,IP报文处理完毕,下一步交给ARP或者直接到数据链路处理。 */
}

9.7.5 IP数据报分片

注意:lwip分片偏移不支持IP首部带选项字段的。

从IP报文首部就可知,有分片概念。

不是每个底层网卡都能承载每个 IP 数据报长度的报文。如:

  • 以太网帧最大能承载 1500 个字节的数据。
  • 某些广域网链路的帧可承载不超过 576 字节的数据。

一个链路层帧能承载的最大数据量叫做最大传送单元(Maximum TransmissionUnit,MTU)。

IP 数据报的分片偏移量是用 8 的整数倍记录的,所以每个数据报中的分片数据大小也必须是 8 的整数倍。

IP数据报分片主要关注IP首部的标识字段、标志字段和分片偏移量字段。具体往前看。

参考图: 【lwip】09-IPv4协议&超全源码实现分析

相关源码实现在ip4_frag.c

相关宏:

  • LWIP_NETIF_TX_SINGLE_PBUF:分片是否支持新建一整个pbuf处理。

    • 1:分片时,直接申请各个IP分片包的pbuf即可(含IP首部+数据区)。
    • 0:分片时,申请各个分片的管理区,MEMP_FRAG_PBUF类型。其数据结构为pbuf_custom_ref。该数据结构包含本次IP分片包的原IP报文pbuf地址,释放引用的api,指向分片IP报文数据区的pbuf。然后将这个分片IP首部的pbuf和这个IP报文数据区的pbuf拼接起来即可。组成新的分片IP报文。

相关数据结构:

pbuf_custom数据结构:

/** A custom pbuf that holds a reference to another pbuf, which is freed
 * when this custom pbuf is freed. This is used to create a custom PBUF_REF
 * that points into the original pbuf. */
struct pbuf_custom_ref {
  /** 'base class' */
  struct pbuf_custom pc; /* 用户的控制区。包含一个pbuf和一个释放该pbuf的api */
  /* 指向被引用的原始pbuf的指针 */
  struct pbuf *original;
};

pbuf_custom_ref数据结构:

struct pbuf_custom {
  /* The actual pbuf */
  struct pbuf pbuf;
  /**This function is called when pbuf_free deallocates this pbuf(_custom) */
  pbuf_free_custom_fn custom_free_function;
};

相关数据结构图:

  • 没开启LWIP_NETIF_TX_SINGLE_PBUF宏的IP分片报文数据结构:

【lwip】09-IPv4协议&超全源码实现分析

  • 开启LWIP_NETIF_TX_SINGLE_PBUF宏的分片IP报文数据结构(按简单的画):

【lwip】09-IPv4协议&超全源码实现分析

ip4_frag()

  • 与分片重组ip4_reass()这个API对应。

  • 需要注意的是:需要检查本次分片处理传入的原IP报文struct pbuf *p是否也是一个分片包。如果是,那么它可能不是顶层原IP报文分片的最后一片,这样的话,在本次分片处理最后一片的分片IP报文首部标志字段的还有更多分片标志位不能置位0。因为在顶层未分片IP报文角度看来,这还不是真正意义上的最后一片。

  • 下面函数分析时,按LWIP_NETIF_TX_SINGLE_PBUF宏分支分析更加助于理解。

  • 处于非LWIP_NETIF_TX_SINGLE_PBUF宏分支:如果读者需要分析,然后看不懂这个分支,可以看下我的笔记:

    • 先申请一个保存分片IP首部的pbuf:rambuf
    • 然后再申请一个pbuf_custom_ref数据结构的伪pbuf:pcr
    • 然后把未分片的IP报文的pbuf对应分片的数据区地址给到pcr->pc->pbuf->payload,共享数据区内存嘛。
    • 然后把分片IP首部的pbuf和分片IP的数据pbuf拼接起来:pbuf_cat(rambuf, pcr->pc->pbuf->payload);,这样就组成了分片的IP报文了。
/**
 * Fragment an IP datagram if too large for the netif.
 *
 * Chop the datagram in MTU sized chunks and send them in order
 * by pointing PBUF_REFs into p.
 *
 * @param p ip packet to send
 * @param netif the netif on which to send
 * @param dest destination ip address to which to send
 *
 * @return ERR_OK if sent successfully, err_t otherwise
 */
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{
  struct pbuf *rambuf; /* 分片的pbuf结构 */
#if !LWIP_NETIF_TX_SINGLE_PBUF
  /* 用于处理分片IP报文与原IP报文数据区pbuf内存共享使用 */
  struct pbuf *newpbuf;
  u16_t newpbuflen = 0;
  u16_t left_to_copy;
#endif
  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; /* 发送的数据在原始数据报pbuf中的偏移量 */
  u16_t tmp;
  int mf_set; /* 传入的pbuf是否是分片包,后续是否还有更多分片。 */

  original_iphdr = (struct ip_hdr *)p->payload;
  iphdr = original_iphdr;
  if (IPH_HL_BYTES(iphdr) != IP_HLEN) { /* IP首部长度字段 */
    /* ip4_frag() 不支持IP首部带选项字段的IP报文分片 */
    return ERR_VAL;
  }
  LWIP_ERROR("ip4_frag(): pbuf too short", p->len >= IP_HLEN, return ERR_VAL);

  /* 保存原始的分片偏移量字段 */
  tmp = lwip_ntohs(IPH_OFFSET(iphdr));
  ofo = tmp & IP_OFFMASK;
  /* 判断pbuf包是否已经被分片,被分片后后续是否还有更多分片。如果有,那么本次分片的最后一个分片不能标志后续没有更多分片。 */
  mf_set = tmp & IP_MF;

  /* 获取还剩多少数据还没分片处理 */
  left = (u16_t)(p->tot_len - IP_HLEN);

  while (left) { /* 循环分片处理 */
    /* 当前分片需要填充的数据size */
    fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));

#if LWIP_NETIF_TX_SINGLE_PBUF /* 分片支持新建整个pbuf */
    /* 申请新的pbuf内存资源装载当前分片 */
    rambuf = pbuf_alloc(PBUF_IP, fragsize, PBUF_RAM);
    if (rambuf == NULL) {
      goto memerr;
    }
    LWIP_ASSERT("this needs a pbuf in one piece!",
                (rambuf->len == rambuf->tot_len) && (rambuf->next == NULL));
    /* 拷贝IP报文数据区的数据到分片IP包中 */
    poff += pbuf_copy_partial(p, rambuf->payload, fragsize, poff);
    /* pbuf腾出IP首部空间 */
    if (pbuf_add_header(rambuf, IP_HLEN)) {
      pbuf_free(rambuf);
      goto memerr;
    }
    /* 填充原IP报文首部到当前分片,后面再处理分片的IP首部 */
    SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
    iphdr = (struct ip_hdr *)rambuf->payload;
#else /* LWIP_NETIF_TX_SINGLE_PBUF */ /* 分片支持数据区共享。这个宏分支,感兴趣的同学可以看下 */
    /* 先申请分片IP首部的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)));
    /* 拷贝原IP报文首部到分片IP报文首部 */
    SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
    iphdr = (struct ip_hdr *)rambuf->payload;

    /* 本次分片IP报文占用的size */
    left_to_copy = fragsize;
    while (left_to_copy) { /* 分片IP报文组装完毕为止 */
      struct pbuf_custom_ref *pcr; /* 引用原IP报文数据区pbuf的伪pbuf */
      u16_t plen = (u16_t)(p->len - poff); /* 原IP报文当前pbuf节点还剩多少数据没处理 */
      LWIP_ASSERT("p->len >= poff", p->len >= poff);
      newpbuflen = LWIP_MIN(left_to_copy, plen); /* 选出能处理的size */

      if (!newpbuflen) { /* 原IP报文当前pbuf所有数据已经分片处理完毕,可以处理下一个pbuf */
        poff = 0;
        p = p->next; /* 跳到下一个分片IP报文处理继续组装当前IP分片 */
        continue;
      }
      pcr = ip_frag_alloc_pbuf_custom_ref(); /* 为伪pbuf pcr申请资源 */
      if (pcr == NULL) {
        pbuf_free(rambuf);
        goto memerr;
      }
      /* 把原IP报文数据区当前分片的数据pbuf引用到pcr->pc->pbuf这个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); /* 当前pbuf引用值+1 */
      pcr->original = p; /* 保存当前pbuf地址到pcr */
      pcr->pc.custom_free_function = ipfrag_free_pbuf_custom; /* 专门的伪pbuf释放API */

      /* 拼接pbuf,把本次分片IP首部、数据区的各个pbuf都链在一起 */
      pbuf_cat(rambuf, newpbuf);
      left_to_copy = (u16_t)(left_to_copy - newpbuflen); /* 检查本次分片IP报文是否整理完毕 */
      if (left_to_copy) { /* 还没填充完毕,需要继续填充 */
        poff = 0;
        p = p->next;
      }
    }
    poff = (u16_t)(poff + newpbuflen); /* 分片在原IP报文中的偏移值更新 */
#endif /* LWIP_NETIF_TX_SINGLE_PBUF */

    /* 更正分片IP首部 */
    /* 本次分片是否为最后一次分片 */
    last = (left <= netif->mtu - IP_HLEN);

    /* 设置新的偏移量和MF标志 */
    tmp = (IP_OFFMASK & (ofo));
    if (!last || mf_set) { /* 本函数分片处理的最后一片 且 传入的原IP报文不是分片报文 */
      /* 标志位第三位更多分片标记为0,已经为顶层原IP报文的最后一片 */
      tmp = tmp | IP_MF;
    }
    IPH_OFFSET_SET(iphdr, lwip_htons(tmp)); /* 重写分片IP首部的标志位字段+分片偏移量字段 */
    IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN))); /* 重写分片IP报文总长度 */
    IPH_CHKSUM_SET(iphdr, 0); /* 设置分片IP报文首部校验字段,默认为0 */
#if CHECKSUM_GEN_IP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {
      IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN)); /* 更新分片IP报文首部校验字段 */
    }
#endif /* CHECKSUM_GEN_IP */

    /* 发送分片IP报文 */
    netif->output(netif, rambuf, dest);
    /* 分片状态信息记录 */
    IPFRAG_STATS_INC(ip_frag.xmit);

    /* Unfortunately we can't reuse rambuf - the hardware may still be
     * using the buffer. Instead we free it (and the ensuing chain) and
     * recreate it next time round the loop. If we're lucky the hardware
     * will have already sent the packet, the free will really free, and
     * there will be zero memory penalty.
     */
    /* 释放分片空间。因为这rambuf只是分片函数内部处理创建的,所以调用netif->output()发送出去后要在这里释放掉。 */
    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;
}

标签:

留言评论

  • 这篇文章还没有收到评论,赶紧来抢沙发吧~