• 四LWIP学习笔记之网际协议(IP)


    一、相关知识

    1、概述

    2、IP地址

      A 类地址中只能有 125 个网络号被分配使用,即全世界只有 125 个机构能使用 A 类网络号,这样的机构通常是很大的机构,因为每个 A 类网络号内包含了上亿个主机地址,通常没有任何一个机构能够使用这么多的主机,因此数以百万计的 A 类网络地址将被浪费掉。

      B 类网络地址(128~191)占据了整个 32bit 地址空间的四分之一,它用前两个字节的后 14bit表示网络号,后两个字节表示该网络内的主机号。B 类网络的网络号有 16384 个,但其中的16 个网络号(172.16 到 172.31)被保留为 B 类网络专用地址,所以可用的 B 类网络号有 16368 个,每个网络能容纳 65534 个主机(216­2)。使用 B 类地址的机构也应该是足够大的机构。 

      C 类网络地址(192~223)占据了整个 32bit 地址空间的八分之一,该类地址用前三个字节中的后21bit 表示网络号,最后一个字节表示该网络内的主机号。C 类地址网络号可达 2097152 个,但其中的 256 个网络号(192.168.0 至 192.168.255)被保留为 C 类网络专用地址,因此可用的 C 类网络号有 2096896 个,每个网络能容纳 254 个主机。C 类网络地址是为具有较少主机和路由器的小型机构设计的,通常该类网络中的主机号往往不够用。 

      D 类网络地址(224~239)占据了整个 32bit 地址空间的十六分之一,是一组专门为组播保留的地址。该类地址并不指向任何特定的网络,每一个 D 类地址定义了在网络中的一组主机,这组机器可以基于这个地址进行多播通信。

      E 类网络地址(224~239)占据了整个 32bit 地址空间的十六分之一,为将来使用而保留的。其中全 1 的 IP 地址(255.255.255.255)用作特殊用途,表示当前子网的广播地址。 

            

      A类网络地址:0.xxx.xxx.xxx、127.xxx.xxx.xxx、10.xxx.xxx.xxx(3)

      B类网络地址:172.16.0.0~172.31.255.255(16)

      C类网络地址:192.168.0.0~192.168.255.255(256)

    3、特殊IP地址

      下面说说几个具有特殊用途的 IP 地址,它们不能分配给任何主机。   

      环回地址 :127.xxx.xxx.xxx(通常使用127.0.0.1),PS:环回地址使得 A 类地址少了一个网络号   

      网络地址 :对任意一个 IP 地址来说,将其地址结构中的主机号全部取 0,就得到了主机所处的网络地址。 例如,某主机 IP 地址为 134.89.32.33,它属于 B 类地址,将其后两个主机号字节全部取 0,则得到这个主机所处的网络地址为 134.89.0.0。 PS:网络地址占用了 A、B、C 三类地址中的每个网络号下的一个主机号。   

      直接广播地址 :在一个网络内,直接广播地址是指对应主机号全部取 1 而得到的 IP 地址,广播地址代表本网络内的所有网络设备,使用该地址可以向属于同一个网段内的所有网络设备传送数据。例如:一个标准 C 类地址 202.197.15.44,由于它的网络号由前面 3 个字节组成,主机号仅是最后一个字节,将主机号位全部取 1 得到的地址是 202.197.15.255,这个地址即是这个网络的广播地址。 A、B、C 三类网络的网络地址和广播地址结构如下所示:  

      A 类主机号:网络地址为:x.0.0.0,广播地址为:x.255.255.255;  

      B 类主机号:网络地址为:x.x.0.0,广播地址为:x.x.255.255;  

      C 类主机号:网络地址为:x.x.x.0,广播地址为:x.x.x.255。   

      这也是为什么在计算表 10­1 中的网络最大主机数时,需要减 2 的原因 

      受限广播地址 :IP 地址 32 位全部为 1,即“255.255.255.255”,代表本地受限广播。该地址用于向本地网络中的所有主机发送广播消息 本网络上的特定主机 ,PS:广播地址本质上是个 E 类地址。 

      本网络上的特定主机 :当用户想与本网络内部特定主机通信时,可以将网络号对应字节全部设置为 0 进行简化。如当具有 B 类地址的某个主机发送数据包时,数据包中的目标 IP 地址为 0.0.11.32,则表示数据包要发送到网络中主机号为 11.32 的主机处。PS:该地址本质上是个 A 类地址。 

      本网络本主机 :IP 地址 32 位全部为 0,即“0.0.0.0”,表示本网络上的本主机。这个地址通常用某个主机启动时,需要通信,但暂时不知道自己的 IP 地址,此时主机为了获得一个有效 IP 地址,将发送一个数 据包给有限广播地址,并用全 0 地址来标志自己。接收方知道发送方还没有 IP 地址,就会采用一种特殊的方式来发送回答,这地址不应该作为目的地址使用。PS:该地址本质上是个 A 类地址。 

      下面讲的这几个专用地址却可以被分配给多个主机,当然这些主机之间应该互不相关,即处于互相独立的专用网内,例如目前很流行的以太网局域网。在每一类地址中部分地址为专用地址,如表 10­2 所示。 

              

    4、子网划分与子网掩码

      现在流行一种扩展的分组编址方案来节省网络号的使用,这种方法称为子网编址,它是一种将网络进一步划分为子网的设计,允许多个物理网络共享一个网络前缀,但每个子网都有自己的子网地址。 

              

      划分子网有诸多的好处:第一,减少标准 IP 编址中地址浪费现象。 第二,节省通信流量。 第三,由于每个网段的主机数减少,使得主机的管理更加方便。 

      子网掩码,标准的 IP 地址,一眼就能够看出 IP 地址的种类(第一个字节就行)并获取其中的网络号,但对于子网编址,要想得到其网络号和子网号,就必须使用子网掩码。子网掩码的长度也是 32 位,左边是网络位(包括子网位),右边是主机位,所有网络位都用 1 表示,所有主机位都用 0 表示,这就形成了子网掩码。 

    5、网络地址转换(NAT)

      企业内部使用的局域网路由器都是具有 NAT 功能的,具有 NAT 功能的路由器至少要有一个内部端口和一个外部端口,内部端口是路由器为了与局域网内的用户通信而使用的,它使用一个内部专用 IP 地址,例如常见的路由器内部 IP 地址可以为 192.168.1.1;外部端口是路由器用来与外部网络通信用的,它通常具有一个有效的 IP 地址,假设为一个有效的 C 类地址 222.197.179.21。NAT 的功能可简单描述为:当内部网络用户连接互联网时,NAT 将用户的内部 IP 地址转换成一个外部公共 IP 地址,反之,数据从外部返回时,NAT 将目的地址替换成用户的内部 IP 地址。 

      假如我们的局域网用户(其使用的专用地址为 192.168.1.78)需要使用 TCP 协议与 Internet 中的一个 HTTP 服务器进行通信(IP 地址为 130.21.45.20,服务端口号为 80),则它将发送一个包含源 IP 地址、源端口号、目的 IP 地址、目的端口号的 IP 分组到路由器处,这里假设这四个值分别为:(192.168.1.78,1234,130.21.45.20,80) 

      NAT转换:(222.178.197.215678130.21.45.2080

      服务器返回:130.21.45.2080222.178.197.215678

      NAT转换:130.21.45.2080192.168.1.781234) 

      这样,经过 NAT 两次简单的转换,局域网用户就实现了与外部网络的数据包交互,上述整个过程对所有用户来说是透明的,但所有局域网用户能够通过同一个 IP 地址与外部进行通信。 

    6、单播、多播与广播

      多播:多播地址是 D 类地址,在 LwIP 中提供了一个宏来判断某个地址是否为多播地址, 通过判断它的最高四位是否为 1110(e)就能知道该地址是不是 D 类多播地址。 

    ————ip_addr.h————————————————
    #define ip_addr_ismulticast(addr1)
    (((addr1)­>addr & ntohl(0xf0000000UL)) == ntohl(0xe0000000UL))
    ————————————————————————————

      广播:在前面讲到广播分为直接广播和有限广播,在以太网中将这两者看作相同的方式,在 LwIP 中,判断一个数据报中的目的地址是否为广播地址的函数叫做ip_addr_isbroadcast 

    ————ip_addr.c——————
    //两个特殊 IP 地址的定义
    #define IP_ADDR_ANY_VALUE 0x00000000UL //某些使用规范中,全 0 也代表所有主机
    #define IP_ADDR_BROADCAST_VALUE 0xffffffffUL //全 1,受限广播地址
    //函数功能:判断一个目的 IP 地址是否是广播地址
    //参数 netif:本地网络接口结构
    //返回值:是广播地址则返回非 0 值
    u8_t ip_addr_isbroadcast(struct ip_addr *addr, struct netif *netif)
    {
      u32_t addr2test;
      addr2test = addr­>addr; //取得 IP 地址结构中的 32 位整数
      if ((~addr2test == IP_ADDR_ANY_VALUE) || //如果 32 位为全 0 或者全 1
        (addr2test == IP_ADDR_ANY_VALUE)) //都是广播地址
        return 1; //返回非 0 值
      //判断是否为受限广播
      else if ((netif­>flags & NETIF_FLAG_BROADCAST) == 0) //如果网络接口不支持广播
        return 0; //则上层应用不应该出现广播地址,直接返回 0
      else if (addr2test == netif­>ip_addr.addr) //如果目的 IP 地址和本接口的 IP 地址一样
        return 0; //也不是广播地址
      //如果目的 IP 地址与本网络接口处于同一子网中,且其主机位全部为 0,则是受限广播
      else if (ip_addr_netcmp(addr, &(netif­>ip_addr), &(netif­>netmask)) //同一网段
        && ((addr2test & ~netif­>netmask.addr) == //且主机位全部为 0
        (IP_ADDR_BROADCAST_VALUE & ~netif­>netmask.addr)))
        return 1; //返回非 0 值
      else
      return 0;
    }
    ————————————————————

      单播:除了组播和广播,剩下的就是单播

    二、数据报

    1、数据报组成结构

      IP 数据报有自己的组织格式,如图 10­4 所示,它通常由两部分组成,即 IP 首部和数据。 

      

      第一个字段是 4bit 的版本号(VER),包含了创建数据报所使用的 IP 协议版本信息,例如对于 IPv4,该值为 4,对于 IPv6,该值为 6。 

      接下来的 4bit 字段用于记录首部长度,这个长度以字为单位。 (最大为15)

      再下来是一个 8bit 的服务类型字段(Type Of Service,TOS),该字段主要用于描述当前 IP 数据报急需的服务类型,如最小延时、最大吞吐量、最高可靠性、最小费用等。路由器在转发数据报时,可以根据这个字段的值来为数据报选择最合理的路由路径。 

      16 位的总长度字段描述了整个 IP 数据报(IP 首部和数据区)的总字节数(一般1500,成为MTU)。当一个很大的 IP 数据报需要发送时,IP 层首先要检查底层接口设备的 MTU,然 后将大的数据报划分为几个分片包,最后分别递交给底层发送; 

      接下来的三个字段:16 位标识字段、3 位标志和 13 位片偏移字段常在 IP 数据报分片时使用。 

      生存时间(TTL)字段描述该 IP 数据报最多能被转发的次数,每经过一次转发,该值会减 1,当该值为 0 时,路由器会丢弃掉分组,同时一个 ICMP 差错报文会被返回至源主机。 

      8 位协议字段和以太网数据帧中的协议类型字段功能相似,不过这里它用来描述该 IP 数据报中的数据是来自于哪个上层协议,例如,该值为 1 表示 ICMP 协议,为 2 表示 IGMP 协议,为 6 表示 TCP协议,为 17 表 UDP 协议。事实上,该字段的值也间接的指出了 IP 数据报的数据区域中数据的格式,因为每一种上层协议都使用了一种独立的数据格式。 

    2、数据结构

      为了方便对 IP 数据报首部字段进行读取或写入操作,在 LwIP 中定义了一个名为 ip_hdr 的结构体来描述数据报首部,如下代码所示: 

    ————ip.h——————————————
    PACK_STRUCT_BEGIN //禁止编译器自对齐
    struct ip_hdr 
    {   PACK_STRUCT_FIELD(u16_t _v_hl_tos);
    //前三个字段:版本号+首部长度+服务类型   PACK_STRUCT_FIELD(u16_t _len); //总长度   PACK_STRUCT_FIELD(u16_t _id); //标识字段   PACK_STRUCT_FIELD(u16_t _offset); //3 位标志位和 13 位片偏移字段   #define IP_RF 0x8000 //标志位第一位(保留位)掩码   #define IP_DF 0x4000 //标志位第二位(不分片标志)掩码   #define IP_MF 0x2000 //标志位第三位(更多分片位)掩码   #define IP_OFFMASK 0x1fff //13 位片偏移字段的掩码   PACK_STRUCT_FIELD(u16_t _ttl_proto); //TTL 字段+协议字段   PACK_STRUCT_FIELD(u16_t _chksum); //首部校验和字段   PACK_STRUCT_FIELD(struct ip_addr src); //源 IP 地址   PACK_STRUCT_FIELD(struct ip_addr dest); //目的 IP 地址 } PACK_STRUCT_STRUCT; PACK_STRUCT_END //下面定义了几个宏,用于对 IP 首部中各个字段值的读取,宏变量 hdr 为指向 //IP 首部结构 ip_hdr 型变量的指针 #define IPH_V(hdr) (ntohs((hdr)­>_v_hl_tos) >> 12) //获取版本号 #define IPH_HL(hdr) ((ntohs((hdr)­>_v_hl_tos) >> 8) & 0x0f) //获取首部长度 #define IPH_TOS(hdr) (ntohs((hdr)­>_v_hl_tos) & 0xff) //获取服务类型 #define IPH_LEN(hdr) ((hdr)­>_len) //获取数据报总长度(网络字节序) #define IPH_ID(hdr) ((hdr)­>_id) //获取数据报标识字段(网络字节序) #define IPH_OFFSET(hdr) ((hdr)­>_offset) //获取标志位+片偏移字段(网络字节序) #define IPH_TTL(hdr) (ntohs((hdr)­>_ttl_proto) >> 8) //获取 TTL 字段 #define IPH_PROTO(hdr) (ntohs((hdr)­>_ttl_proto) & 0xff) //获取协议字段 #define IPH_CHKSUM(hdr) ((hdr)­>_chksum) //获取首部校验和字段(网络字节序) //下面定义几个宏,用于填写 IP 首部各个字段的值,宏变量 hdr 为指向 //IP 首部结构 ip_hdr 型变量的指针 #define IPH_VHLTOS_SET(hdr, v, hl, tos) (hdr)­>_v_hl_tos = //版本号+首部长度+服务类型 (htons(((v) << 12) | ((hl) << 8) | (tos))) #define IPH_LEN_SET(hdr, len) (hdr)­>_len = (len) //数据报总长度(len 应为网络字节序) #define IPH_ID_SET(hdr, id) (hdr)­>_id = (id) //数据报标识字段(id 应为网络字节序) #define IPH_OFFSET_SET(hdr, off) (hdr)­>_offset = (off) //标志位+片偏移字段 #define IPH_TTL_SET(hdr, ttl) (hdr)­>_ttl_proto = //TTL 字段 (htons(IPH_PROTO(hdr) | ((u16_t)(ttl) << 8))) #define IPH_PROTO_SET(hdr, proto) (hdr)­>_ttl_proto = //协议字段 (htons((proto) | (IPH_TTL(hdr) << 8))) #define IPH_CHKSUM_SET(hdr, chksum) (hdr)­>_chksum = (chksum) //首部校验和 ————————————————————————————————————

    三、IP层输出

    1、发送数据报

      当传输层协议(TCP 或 UDP)要发送数据时,它们会将数据按照自己的格式组装在一个 pbuf中,并将 payload 指针指向协议首部,然后调用 IP 层的数据报发送函数 ip_output 发送数据。 然后调用函数 ip_output_if 将数据报发送出去。 

    ————ip.c——————————————
    //函数功能:被传输层协议调用以发送数据报,该函数查找网络接口并调用函数
    // ip_output_if 完成最终的发送工作
    //参数 p:传输层协议需要发送的数据包 pbuf,payload 指针已指向协议首部
    //参数 src:源 IP 地址,若为 NULL,则使用网络接口结构中保存的 IP 地址
    //参数 dest:目的 IP 地址,若为 IP_HDRINCL,则表示 p 中已经有了填写好的 IP
    //数据报首部,且 payload 指针也已经指向了 IP 首部
    //参数 ttl、tos、proto:IP 首部中的 TTL 字段、服务类型和协议类型值
    err_t ip_output(struct pbuf *p, struct ip_addr *src, struct ip_addr *dest,u8_t ttl, u8_t tos, u8_t proto)
    {
      struct netif *netif;
      //根据目的 IP 地址为数据报寻找一个合适的网络接口
      if ((netif = ip_route(dest)) == NULL) 
      { //若找到,变量 netif 指向对应的接口结构     return ERR_RTE; //找不到,返回接口错误   }   //否则,增加 netif 为参数调用函数 ip_output_if   return ip_output_if(p, src, dest, ttl, tos, proto, netif); } //函数功能:填写 IP 首部中的各个字段值(不处理选项字段),并发送数据报 //参数说明:netif 为发送数据报的网络接口结构,其他参数与 ip_output 的相同 err_t ip_output_if(struct pbuf *p, struct ip_addr *src, struct ip_addr *dest,u8_t ttl, u8_t tos, u8_t proto, struct netif *netif) {   struct ip_hdr *iphdr;   static u16_t ip_id = 0; //静态变量,记录 IP 数据报的编号(标识字段)   if (dest != IP_HDRINCL)
      { //dest 不为 IP_HDRINCL,说明 pbuf 中未填写 IP 首部     u16_t ip_hlen = IP_HLEN; //宏 IP_HLEN 为默认的 IP 首部长度,20     if (pbuf_header(p, IP_HLEN))
        { //移动 payload 指针,指向 pbuf 中的 IP 首部       return ERR_BUF; //失败,返回 pbuf 空间错误     }     iphdr = p­>payload; //iphdr 指向数据报首部     IPH_TTL_SET(iphdr, ttl); //填写 TTL 字段     IPH_PROTO_SET(iphdr, proto); //填写协议字段     ip_addr_set(&(iphdr­>dest), dest); //填写目的 IP 地址     IPH_VHLTOS_SET(iphdr, 4, ip_hlen / 4, tos); //填写版本号+首部长度+服务类型     IPH_LEN_SET(iphdr, htons(p­>tot_len)); //填写数据报总长度     IPH_OFFSET_SET(iphdr, 0//填写标志位和片偏移字段,都为 0     IPH_ID_SET(iphdr, htons(ip_id)); //填写标识字段     ++ip_id; //数据报编号值加 1     if (ip_addr_isany(src))
        { //若 src 为空,则将源 IP 地址填写为网络接口的 IP 地址       ip_addr_set(&(iphdr­>src), &(netif­>ip_addr));     } else
        { //若 src 不为空,则直接填写源 IP 地址       ip_addr_set(&(iphdr­>src), src);     }     IPH_CHKSUM_SET(iphdr, 0//清 0 校验和字段     IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, ip_hlen)); //计算并填写首部校验和   } else
      { //如果 pbuf 中已经填写 IP 首部,这里不需要填写任何字段     iphdr = p­>payload;     dest = &(iphdr­>dest); //只是用变量 dest 记录下数据报中的目的 IP 地址   }   //下面开始发送 IP 数据报,分为三种情况来处理   if (ip_addr_cmp(dest, &netif­>ip_addr))
      { //如果目的 IP 地址是本网卡的地址,     return netif_loop_output(netif, p, dest); //则调用环回输入,这个函数见第 8 章   }   if (netif­>mtu && (p­>tot_len > netif­>mtu))
      { //如果数据报太大(大于接口的 mtu)     return ip_frag(p,netif,dest); //则调用函数 ip_frag 对数据报分片发送   }   //剩下的情况,直接调用接口注册的 output 函数发送数据报   return netif­>output(netif, p, dest); } —————————————————————————————————

      函数 ip_output 中还出现了一个 ip_route 函数,它比较简单,功能是根据目的 IP 地址在系统的所有网络接口结构中选择一个最佳的网络接口返回,该网络接口将用于发送最终的数据报。 

    ————ip.c——————————————————
    //函数功能:根据目的 IP 地址选择一个最合适的网络接口结构
    //参数 dest:目的 IP 地址
    //返回值:指向相应网络接口结构的指针
    struct netif * ip_route(struct ip_addr *dest)
    {
      struct netif *netif;
      for(netif = netif_list; netif != NULL; netif = netif­>next) 
      {//依次遍历接口结构链表     if (netif_is_up(netif))
        { //接口已经使能       if (ip_addr_netcmp(dest, &(netif­>ip_addr), &(netif­>netmask)))
          {//且与目的 IP 地址         return netif; //主机处于同一子网内,则返回这个接口结构       }     }   }//for   //到这里,说明没找到合适的接口,我们把系统的默认网络接口作为结果返回   if ((netif_default == NULL) || (!netif_is_up(netif_default)))
      {//如果默认网络接口未配置或     return NULL; //未使能,则返回空指针   }   return netif_default; //返回默认网络接口 } ——————————————————————————————————

      从上面的代码看出,所谓最佳的网络接口,就是与目的 IP 地址处于统一子网的网络接口,这一点的判断需要使用到子网掩码的概念。 

    2、数据报分片

      

            总长度(16位)  标识(16位)  标志(3位)  分片偏移量(13位)

      原始数据报   3020      12345      0        0

      分片1      1420      12345      1        0

      分片2      1420      12345      1        175(1400/8)

      分片3       220       12345      0        350(2800/8)

      16 位标识字段:用于标识 IP 层发送出去的每一份 IP 数据报,每发送一份报文,则该值加 1,数据包被分片时,该字段会被复制到每一个分片中。在接收端,会使用这个字段值来组装所有分片为一个完整的数据报。

      3位标志字段:第一位保留;第二位是不分片位;第三位表示更多分片位,当该位被置1表示该分片不是最后一分片,当该为被置0表示该分片为最后一分片

      13位分片偏移量:相对于原始数据报以8个字节为单位的偏移量

    ————ip_frag.c————————————————————
    //定义一个全局型的数组,数组大小为 IP 层允许的最大分片大小,每个分片会被先后
    //拷贝到这个数组中,然后发送
    #if IP_FRAG_USES_STATIC_BUF
    static u8_t buf[LWIP_MEM_ALIGN_SIZE(IP_FRAG_MAX_MTU + MEM_ALIGNMENT ­ 1)];
    #endif
    //函数功能:将数据报 p 进行分片发送,该函数在 ip_output_if 中被调用
    //参数 p:需要分片发送的数据报
    //参数 netif:发送数据报的网络接口结构指针
    //参数 dest:目的 IP 地址
    err_t ip_frag(struct pbuf *p, struct netif *netif, struct ip_addr *dest)
    {
      struct pbuf *rambuf; //分片的 pbuf 结构
      struct pbuf *header; //以太网帧 pbuf
      struct ip_hdr *iphdr; //IP 首部指针
      u16_t nfb; //分片中允许的最大数据量
      u16_t left, cop; //待发送数据长度和当前发送的数据长度
      u16_t mtu = netif­>mtu; //网络接口 MTU
      u16_t ofo, omf; //分片偏移量和更多分片位
      u16_t last; //是否为最后一个分片
      u16_t poff = IP_HLEN; //发送的数据在原始数据报 pbuf 中的偏移量
      u16_t tmp;
      rambuf = pbuf_alloc(PBUF_LINK, 0, PBUF_REF); //为数据分片申请一个 pbuf 结构
      if (rambuf == NULL) { //申请失败,则返回
        return ERR_MEM;
      }
      rambuf­>tot_len = rambuf­>len = mtu; //设置 pbuf 的 len 和 tot_len 字段为接口的 MTU 值
      rambuf­>payload = LWIP_MEM_ALIGN((void *)buf); //payload 指向全局数据区域
      iphdr = rambuf­>payload; //得到分片包存储区域
      SMEMCPY(iphdr, p­>payload, IP_HLEN); //把原始数据报首部拷贝到分片的首部
      tmp = ntohs(IPH_OFFSET(iphdr)); //暂存分片的相关字段
      ofo = tmp & IP_OFFMASK; //得到分片偏移量(对原始数据报来说应该为 0)
      omf = tmp & IP_MF; //得到更多分片标志值
      left = p­>tot_len ­ IP_HLEN; //待发送数据长度(总长度­IP 首部长度)
      nfb = (mtu ­ IP_HLEN) / 8; //一个分片中可以存放的最大数据量(8 字节为单位)
      while (left) { //待发送数据长度大于 0
        last = (left <= mtu ­ IP_HLEN); //若待发送长度小于分片最大长度,last 为 1,否则为 0
        tmp = omf | (IP_OFFMASK & (ofo)); //计算分片相关字段
        if (!last) //如果不是最后一个分片
          tmp = tmp | IP_MF; //则更多分片位置 1
        //计算分片中的数据长度,当 last 为 1 时,说明分片能装下所有剩余数据
        cop = last ? left : nfb * 8;
        //从原始数据报中拷贝 cop 字节的数据到分片中,poff 记录了拷贝起始位置
        poff += pbuf_copy_partial(p, (u8_t*)iphdr + IP_HLEN, cop, poff);
        //填写分片首部中的其他字段
        IPH_OFFSET_SET(iphdr, htons(tmp)); //填写分片相关字段
        IPH_LEN_SET(iphdr, htons(cop + IP_HLEN)); //填写总长度
        IPH_CHKSUM_SET(iphdr, 0//清 0 校验和
        IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN)); //计算校验和
        if (last) //若为最后一个分片,则更改 pbuf 的 len 和 tot_len 字段
          pbuf_realloc(rambuf, left + IP_HLEN);
        //到这里,我们组装好了一个完整的分片,需要将该分片发送出去,这里重新在
        //内存堆中开辟一个 pbuf 空间,用来保存以太网帧首部
        header = pbuf_alloc(PBUF_LINK, 0, PBUF_RAM);
        if (header != NULL) { //申请成功,则做发送工作
          pbuf_chain(header, rambuf); //将两个 pbuf 连接成一个 pbuf 链表
          netif­>output(netif, header, dest); //调用函数发送
          pbuf_free(header); //发送完成后,释放 pbuf 链表
        } else { //若以太网首部空间申请失败
          pbuf_free(rambuf); //释放分片空间
          return ERR_MEM; //返回内存错误
        }
        left ­= cop; //待发送的总长度减少
        ofo += nfb; //分片偏移量增加
      }//while
      pbuf_free(rambuf); //释放结构 rambuf
      return ERR_OK;
    }
    ——————————————————————————————————

    四、IP层输入

    1、数据报接收

    2、分片重装数据结构

    3、分片重装函数

    4、分片插入与检查

  • 相关阅读:
    汉字在屏幕上的显示
    手机上的ROM与RAM
    数据表示和计算
    存储器的层次结构
    计算机系统概述
    Python中的文件路径的分隔符
    网络爬虫的基本原理
    iOS多线程简介
    Quartz2D简介
    iOS 事件传递响应链
  • 原文地址:https://www.cnblogs.com/qinzhou/p/8378516.html
Copyright © 2020-2023  润新知