深入學習IP數據報發送過程

IP協議的功能

回顧一下前面的文章所提及的知識點,總結一下IP協議的功能,得到以下結論:

  1. 編址(目標端的IP地址),數據傳輸的過程當中就必須表明要發送目標端的IP地址
  2. 尋址和路由(根據對方的IP地址,尋找最佳路徑傳輸信息);
  3. 數據報的分片和重組。
  4. 傳遞服務是不可靠的(IP協議只是儘自己最大努力去傳輸數據包),它也是無連接的協議

IP數據報發送

IP協議是網絡層的主要協議,在上層傳輸協議(如TCP/UDP)需要發送數據時,會將數據封裝起來,然後傳遞到IP層,IP協議首先會根據上層協議的目標IP地址選擇一個合適的網卡進行發送數據(路由),然後IP協議將再次封裝數據形成IP數據報,主要的操作就是填寫IP數據報首部對應的各個字段:目標IP地址、源IP地址、協議類型、生存時間等,最後在IP層通過回調函數netif->output(即etharp_output()函數)將IP數據報投遞給ARP,再調用網卡底層發送函數進行發送,這樣子自上而下的數據就發送出去,IP協議以目標IP地址作爲目標主機的身份地址。

/**
 * ip_output_if的簡化版接口。它找到發送數據包的netif網絡接口並調用ip_output_if來完成實際工作。
 *
 * @param p 要發送的數據包(p->payload(有效負載)指向數據,如果dest == LWIP_IP_HDRINCL,則p已包含IP頭和p->有效負載指向該IP頭)
 * @param src 要發送的源IP地址(如果src == IP4_ADDR_ANY,則用發送的netif綁定的IP地址用作源地址)
 * @param dest 目的IP地址
 * @param ttl 要在IP標頭中設置的TTL值(生存時間)
 * @param tos 用於在IP標頭中設置的TOS值
 * @param proto 將在IP頭中設置對應的上層協議
 * @return ERR_OK 如果數據包發送正常就返回ok,
 *         如果p沒有足夠的空間用於IP /LINK標頭,則爲ERR_BUF
 *         其他則返回netif->output返回的錯誤
 * @return ERR_RTE如果沒有找到路線
 * 請參閱ip_output_if()以獲取更多返回值
 */ 
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;

  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;
  }

  return ip4_output_if(p, src, dest, ttl, tos, proto, netif);
}

路由過程的實現

路由(routing)就是通過互聯的網絡把信息從源地址傳輸到目的地址的活動,發送端必然需要找到一個網卡將數據報發送出去,而實現這個過程的函數就是ip4_route_src()

其實lwip對ip4_route_src()函數進行了重新定義,實際上是調用了ip4_route()函數。這個函數的原理就是根據指定的IP地址找到合適的網卡netif,然後返回,前面的文章也提到過,lwip的網卡是通過netif_list列表管理的,那麼找網卡的操作也必然是遍歷網卡列表netif_list,判斷網卡是否已經掛載並且IP地址是否有效,如果連網卡都找不到,那就不用發送數據了,返回null。

#define ip4_route_src(src, dest) ip4_route(dest)
/**
 *爲給定的IP地址查找適當的網絡接口。
 *它搜索網絡接口列表。找到匹配項
 *
 *@param dest 要查找路由的目標IP地址
 *@return 發送到達目的地的網卡 netif
 */ 
struct netif *
ip4_route(const ip4_addr_t *dest)
{
#if !LWIP_SINGLE_NETIF
  struct netif *netif;

  LWIP_ASSERT_CORE_LOCKED();

#if LWIP_MULTICAST_TX_OPTIONS
  /*默認使用管理選擇的接口進行多播*/
  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_list */
  NETIF_FOREACH(netif) {
    /* 如果網卡已經掛載並且IP地址是有效的 */
    if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) {
      /* 網絡掩碼匹配? */
      if (ip4_addr_netcmp(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) {
        /* 返回找到的網卡netif */
        return netif;
      }
      /* 網關在非廣播接口上匹配? (即在點對點接口中對等) */
      if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_cmp(dest, netif_ip4_gw(netif))) {
        /* 返回找到的網卡netif */
        return netif;
      }
    }
  }

#if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF    /**如果打開環回地址的宏定義 */
  /* loopif is disabled, looopback traffic is passed through any netif */
  if (ip4_addr_isloopback(dest)) {
    /*不檢查環迴流量的鏈接*/ 
    if (netif_default != NULL && netif_is_up(netif_default)) {
      return netif_default;
    }
    /*默認netif沒有啓動,只需使用任何netif進行環迴流量*/ 
    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)) {
    /*找不到匹配的netif,默認的netif不可用。建議使用LWIP_HOOK_IP4_ROUTE()*/ 
    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;
}

ip4_output_if

找到網卡之後就調用ip4_output_if()函數將數據發送出去,這個函數會指定發送數據的網卡,同時會將來自上層協議(tcp、udp)的數據進行封裝,組成IP數據報再發送,不過這個函數層層調用,比較麻煩,具體如下:。

/**
 * 在網絡接口上發送IP數據包。這個函數構造IP數據包首部並計算IP頭校驗和,
 * 如果源IP地址爲NULL,在發送的時候就填寫發送網卡的IP地址爲源IP地址
 * 如果目標IP地址是LWIP_IP_HDRINCL,則假定pbuf已經存在包括IP頭和有效負載指向它而不是數據。
 * 
 * @param p 要發送的數據包(p->payload(有效負載)指向數據,如果dest == LWIP_IP_HDRINCL,則p已包含IP頭和p->有效負載指向該IP頭)
 * @param src 要發送的源IP地址(如果src == IP4_ADDR_ANY,則用發送的netif綁定的IP地址用作源地址)
 * @param dest 目的IP地址
 * @param ttl 要在IP標頭中設置的TTL值(生存時間)
 * @param tos 用於在IP標頭中設置的TOS值
 * @param proto 將在IP頭中設置對應的上層協議
 * @param netif 發送此數據包的netif
 * @return ERR_OK 如果數據包發送正常就返回ok,
 *         如果p沒有足夠的空間用於IP /LINK標頭,則爲ERR_BUF
 *         其他則返回netif->output返回的錯誤
 *
 * @note ip_id:RFC791“某些主機可能只需使用
 * 獨立於目的地的唯一標識符“
 */ 
err_t
ip4_output_if(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)
{
#if IP_OPTIONS_SEND
  return ip4_output_if_opt(p, src, dest, ttl, tos, proto, netif, NULL, 0);
}
/**
 * 與ip_output_if()相同,但可以包含IP選項:
 *
 * @param ip_options指向IP選項的指針,複製到IP頭中
 * @param optlen ip_options的長度
 */ 
err_t
ip4_output_if_opt(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 */
  const ip4_addr_t *src_used = src;
  if (dest != LWIP_IP_HDRINCL) {
    if (ip4_addr_isany(src)) {
      src_used = netif_ip4_addr(netif);
    }
  }

#if IP_OPTIONS_SEND
  return ip4_output_if_opt_src(p, src_used, dest, ttl, tos, proto, netif,
                               ip_options, optlen);
#else /* IP_OPTIONS_SEND */
  return ip4_output_if_src(p, src_used, dest, ttl, tos, proto, netif);
#endif /* IP_OPTIONS_SEND */
}

ip4_output_if_opt_src

首先看看這個函數到底做了什麼吧:在上層協議遞交數據包後,通過層層調用,最終到ip4_output_if_opt_src()函數中處理,它的處理如下:
在這裏插入圖片描述
代碼的實現如下:註釋非常豐富。主要過程就是:

  1. 判斷是否填寫好IP數據報首部?若目標IP地址爲LWIP_IP_HDRINCL表示已經填寫好IP數據報首部,且payload指針也指向了IP數據報首部。
  2. 如果沒有填寫IP數據報首部,調用pbuf_add_header()函數調整數據區域指針以指向IP數據報首部。
  3. 填寫IP數據報中的生存時間、服務類型、上層協議、目標IP地址、版本號與首部長度、數據報總長度、標誌位和分片偏移量、標識、源IP地址等內容,總之就是將IP數據報首部的內容該填的都填上。
  4. 如果目標IP地址是自己的網卡IP地址,調用環回輸入函數netif_loop_output()發送IP數據報給自己,這種處理一般是用於測試代碼。
  5. 如果IP數據報太大,數據報總長度大於網卡的MTU,則需要進行分片處理,調用ip4_frag()函數進行發送。
  6. 直接調用註冊的netif->output接口傳遞給ARP,實際上就是調用etharp_output()函數,在這裏它會將IP地址解析成對應的MAC地址,並且調用網卡發送函數進行發送。
/**
 * 與ip4_output_if_opt()相同,當源地址是'IP4_ADDR_ANY'時,'src'地址不會被netif地址替換
 */ 
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();
  LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p);

  MIB2_STATS_INC(mib2.ipoutrequests);

  /* 應該要構建IP首部還是已經包含在pbuf中了?如果是要構建IP數據報首部 */
  if (dest != LWIP_IP_HDRINCL) {
    u16_t ip_hlen = IP_HLEN;
#if IP_OPTIONS_SEND
    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)) {
        /* 選項字段太長 */
        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;
      }
      /* 選項字段按照4字節對齊 */
      optlen_aligned = (u16_t)((optlen + 3) & ~3);
      ip_hlen = (u16_t)(ip_hlen + optlen_aligned);
      /* 首先寫入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"));
        IP_STATS_INC(ip.err);
        MIB2_STATS_INC(mib2.ipoutdiscards);
        return ERR_BUF;
      }
      MEMCPY(p->payload, ip_options, optlen);
      if (optlen < optlen_aligned) {
        /* 剩餘字節清零 */
        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頭 */
    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"));

      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);
    IPH_PROTO_SET(iphdr, proto);
#if CHECKSUM_GEN_IP_INLINE
    chk_sum += PP_NTOHS(proto | (ttl << 8));
#endif /* CHECKSUM_GEN_IP_INLINE */

    /* 構建目的IP地址,此處的目的IP地址不能爲NULL */
    ip4_addr_copy(iphdr->dest, *dest);  
#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);
    IPH_TOS_SET(iphdr, 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;

    if (src == NULL) {
      ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);    /** 構建源IP地址 */
    } else {
      /* 此處的源IP地址不能爲NULL */
      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;
    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) {
      iphdr->_chksum = (u16_t)chk_sum; /* network order */
    }
#if LWIP_CHECKSUM_CTRL_PER_NETIF
    else {
      IPH_CHKSUM_SET(iphdr, 0);
    }
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
#else /* CHECKSUM_GEN_IP_INLINE */
    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 /* CHECKSUM_GEN_IP */
#endif /* CHECKSUM_GEN_IP_INLINE */
  } else {
    /* IP頭已包含在pbuf中 */
    if (p->len < IP_HLEN) {   
      /** pbuf的長度小於IP數據報首部長度(20字節) */
      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;    /** 直接從數據區域獲取IP數據報首部 */
    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_cmp(dest, netif_ip4_addr(netif))
#if !LWIP_HAVE_LOOPIF
      || ip4_addr_isloopback(dest)
#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) {
    netif_loop_output(netif, p);
  }
#endif /* LWIP_MULTICAST_TX_OPTIONS */
#endif /* ENABLE_LOOPBACK */
#if IP_FRAG
  /** 要發送的數據報大於mtu,需要分片,此處的前提是使能了IP_FRAG (IP分片) */
  if (netif->mtu && (p->tot_len > netif->mtu)) {
    return ip4_frag(p, netif, dest);  /** 調用IP數據報分片函數將數據報分片發送出去 */
  }
#endif /* IP_FRAG */

  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));
  return netif->output(netif, p, dest);   /** 如果不需要分片就直接通過網卡發送出去,netif->output() */
}

最後提個醒

此外:上層協議是不會直接調用ip4_output()函數的,lwip是通過宏定義將ip4_output()函數進行重新定義:

#define ip_output(p, src, dest, ttl, tos, proto)    ip4_output(p, src, dest, ttl, tos, proto)

我github上有一個開源項目就是翻譯lwip v2.1.2版本源碼,本文的源碼註釋出自這裏,我自己的時間有限,翻譯起來會很慢很慢,歡迎有興趣的小夥伴可以參與進來:https://github.com/jiejieTop/LwIP_2.1.2_Comment

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章