深入 LWIP:数据是如何被封装并发送出去的

1、一条数据的旅程

当应用程序调用:

c 复制代码
send(sock, "hello", 5, 0);

这条数据会在协议栈中经历以下几个层次:

  • 应用层接口(socket API)
  • 传输层(UDP)
  • 网络层(IP)
  • 数据链路层(Ethernet)
  • 网卡驱动层

最终数据被封装成 以太网帧,通过网卡发送到物理网络。

send

udp_sendto

ip_output

etharp_output

ethernet_output

netif->linkoutput

2、具体函数详解

本篇文章皆以 UDP 数据包为例进行讲解,去除了一些高级功能,更便于理解。

2.1 应用层

send -> lwip_sendto

该函数主要功能:

  • 处理应用层的数据
    • 情况1:调用 LWIP 协议中接口 netbuf_alloc 分配 pbuf 以及 pbuf 数据缓冲区 payload 。并将用户数据拷贝到 pbuf-> payload 中
    • 情况2:LWIP 创建一个 pbuf,pbuf 的 payload 直接指向应用程序提供的缓冲区(data 指针)
  • 调用 netconn_send 发送数据
c 复制代码
ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,
            const struct sockaddr *to, socklen_t tolen)
{
  struct lwip_sock *sock;
  err_t err;
  u16_t short_size;
  u16_t remote_port;
  struct netbuf buf;

  // 【1】获取套接字对象,失败则返回 -1
  sock = get_socket(s);
  if (!sock) {
    return -1;
  }

  // 【2】判断连接类型:如果是 TCP,则调用 lwip_send(忽略目标地址)
  if (NETCONNTYPE_GROUP(netconn_type(sock->conn)) == NETCONN_TCP) {
#if LWIP_TCP
    done_socket(sock);
    return lwip_send(s, data, size, flags);  // TCP 不需要目标地址
#else /* LWIP_TCP */
    LWIP_UNUSED_ARG(flags);
    sock_set_errno(sock, err_to_errno(ERR_ARG));
    done_socket(sock);
    return -1;
#endif /* LWIP_TCP */
  }

  // 【3】检查数据大小是否超过单个数据报上限(65535 字节)
  if (size > LWIP_MIN(0xFFFF, SSIZE_MAX)) {
    sock_set_errno(sock, EMSGSIZE);  // 设置错误码:消息太大
    done_socket(sock);
    return -1;
  }
  short_size = (u16_t)size;
  ......

  // 【4】初始化网络缓冲区 netbuf
  buf.p = buf.ptr = NULL;

  // 【5】解析目标地址和端口;若无目标地址,则使用任意地址(IPv4/IPv6)
  if (to) {
    SOCKADDR_TO_IPADDR_PORT(to, &buf.addr, remote_port);  // 宏:提取 IP 和端口
  } else {
    remote_port = 0;
    ip_addr_set_any(NETCONNTYPE_ISIPV6(netconn_type(sock->conn)), &buf.addr);
  }
  netbuf_fromport(&buf) = remote_port;
  ......

  // 【6】准备数据缓冲区:根据配置选择"拷贝"或"引用"方式
#if LWIP_NETIF_TX_SINGLE_PBUF
  // 模式 A:分配新 pbuf 并拷贝数据
  if (netbuf_alloc(&buf, short_size) == NULL) {
    err = ERR_MEM;  // 内存不足
  } else {
    {
      MEMCPY(buf.p->payload, data, short_size);  // 普通内存拷贝
    }
    err = ERR_OK;
  }
#else /* LWIP_NETIF_TX_SINGLE_PBUF */
  // 模式 B:直接引用用户数据(可能会带来性能损失和代码复杂度)
  err = netbuf_ref(&buf, data, short_size);
#endif /* LWIP_NETIF_TX_SINGLE_PBUF */

  if (err == ERR_OK) {
   ......
    // 【7】调用底层 netconn_send 发送数据
    err = netconn_send(sock->conn, &buf);
  }

  // 【8】释放 netbuf 资源(无论成功与否)
  netbuf_free(&buf);

  // 【9】设置 errno 并返回结果:成功返回发送字节数,失败返回 -1
  sock_set_errno(sock, err_to_errno(err));
  done_socket(sock);
  return (err == ERR_OK ? short_size : -1);
}

2.2 传输层(UDP)

netconn_send -> lwip_netconn_do_send -> udp_sendto

在 UDP 层,所有连接信息由 udp_pcb(Protocol Control Block)维护,包括:

  • 本地端口
  • 远端地址
  • 回调函数
  • 网络接口绑定信息

udp_pcb 结构在 lwip_socket 创建 sock 接口时创建

该函数主要功能:

  • 根据目标 IP 地址查找合适的网络接口(netif)
  • 调用底层发送函数 udp_sendto_if
c 复制代码
err_t
udp_sendto(struct udp_pcb *pcb, struct pbuf *p,
           const ip_addr_t *dst_ip, u16_t dst_port)
{
  struct netif *netif;
  ......
  ......

  /* 【确定 outgoing network interface (发送网口)】
   * 逻辑分支:判断 PCB 是否已经指定了具体的网络接口索引 (netif_idx)。
   */
  if (pcb->netif_idx != NETIF_NO_INDEX) {
    /* 如果指定了接口索引,直接通过索引获取 netif 指针 */
    netif = netif_get_by_index(pcb->netif_idx);
  } else {
    /* 如果没有指定具体接口,需要根据路由规则自动选择 */
#if LWIP_MULTICAST_TX_OPTIONS
    netif = NULL;
    /*  如果目标地址是组播地址 (Multicast),需要特殊处理接口选择。*/
    if (ip_addr_ismulticast(dst_ip)) {
      /* 优先检查 PCB 是否指定了组播出口接口索引 (mcast_ifindex) */
      if (pcb->mcast_ifindex != NETIF_NO_INDEX) {
        netif = netif_get_by_index(pcb->mcast_ifindex);
      }
#if LWIP_IPV4
      else
#if LWIP_IPV6
        if (IP_IS_V4(dst_ip))
#endif /* LWIP_IPV6 */
        {
          /* 
           * IPv4 组播默认不基于源路由,但可以通过 mcast_ip4 覆盖。
           * 如果 pcb->mcast_ip4 被设置且不是任意地址或广播地址,
           * 则尝试根据该源地址进行路由查找,以确定出口接口。
           */
          if (!ip4_addr_isany_val(pcb->mcast_ip4) &&
              !ip4_addr_cmp(&pcb->mcast_ip4, IP4_ADDR_BROADCAST)) {
            netif = ip4_route_src(ip_2_ip4(&pcb->local_ip), &pcb->mcast_ip4);
          }
        }
#endif /* LWIP_IPV4 */
    }

    /* 如果经过上述组播逻辑仍未找到 netif (netif == NULL),
     * 或者当前不是组播包,则 fallback 到下面的常规路由查找。
     */
    if (netif == NULL)
#endif /* LWIP_MULTICAST_TX_OPTIONS */
    {
      /* 【常规路由查找】
       * 调用 ip_route(),根据本地IP (pcb->local_ip) 和目标IP (dst_ip)
       * 查找系统路由表,决定从哪个网络接口发送数据包。
       */
      netif = ip_route(&pcb->local_ip, dst_ip);
    }
  }
  ......
  if (netif == NULL) {
    LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("udp_send: No route to "));
    ip_addr_debug_print(UDP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, dst_ip);
    LWIP_DEBUGF(UDP_DEBUG, ("\n"));
    
    /* 统计路由错误次数 */
    UDP_STATS_INC(udp.rterr);
    /* 返回"无路由"错误 */
    return ERR_RTE;
  }

  /* 【执行发送】
   * 找到了合法的 netif,调用底层发送函数。
   * 选择调用带校验和参数的版本 (udp_sendto_if_chksum) 或普通版本 (udp_sendto_if)。
   */
 ......
 return udp_sendto_if(pcb, p, dst_ip, dst_port, netif);
}

udp_sendto_if

该函数主要功能:

  • 在已经确定了出口网络接口 (netif) 的前提下,确定数据包的"源IP地址",并调用底层函数将 UDP 数据包发送出去
c 复制代码
err_t
udp_sendto_if(struct udp_pcb *pcb, struct pbuf *p,
              const ip_addr_t *dst_ip, u16_t dst_port, struct netif *netif)
{
  const ip_addr_t *src_ip; /* 用于存储最终确定的源IP地址指针 */
  ......
  ......

  /* 【确定源IP地址 (Source IP Selection)】
   * 这是本函数最核心的逻辑。
   * 需要决定数据包的源 IP 是用 PCB 中绑定的 local_ip,还是用网卡的当前 IP。
   */
#if LWIP_IPV6
  /* --- IPv6 处理逻辑 --- */
  if (IP_IS_V6(dst_ip)) {
      ......
  }
#endif /* LWIP_IPV6 */

#if LWIP_IPV4 && LWIP_IPV6
  else
#endif /* LWIP_IPV4 && LWIP_IPV6 */

#if LWIP_IPV4
  /* --- IPv4 处理逻辑 --- */
  if (ip4_addr_isany(ip_2_ip4(&pcb->local_ip)) ||
      ip4_addr_ismulticast(ip_2_ip4(&pcb->local_ip))) {
    
    /* 情况 A: PCB 绑定的本地地址是 0.0.0.0 (ANY) 或 组播地址。
     * 策略:直接使用出口网卡 (netif) 当前配置的 IPv4 地址作为源地址。
     */
    src_ip = netif_ip_addr4(netif);
  } 
  else {
    /* 情况 B: PCB 绑定了具体的 IPv4 地址。
     * 策略:验证该地址是否仍然是当前网卡的IP。
     * 背景:如果网卡 IP 通过 DHCP 改变了,而应用层还在用旧 IP 发送,这里会拦截。
     */
    if (!ip4_addr_cmp(ip_2_ip4(&(pcb->local_ip)), netif_ip4_addr(netif))) {
      /* 本地 IP 与网卡当前 IP 不匹配,视为路由错误,丢弃数据包 */
      return ERR_RTE;
    }
    /* 匹配成功,使用 PCB 中绑定的地址作为源IP */
    src_ip = &pcb->local_ip;
  }
#endif /* LWIP_IPV4 */

  /* 【执行最终发送】
 * 此时 src_ip 已确定。
 * 注意:真正的硬件发送操作(填充以太网头、IP头、UDP头、校验和计算、推送到驱动)
 * 将在这些 *_src_* 函数中完成。
   */
  return udp_sendto_if_src(pcb, p, dst_ip, dst_port, netif, src_ip);
  ......
}

udp_sendto_if_src

是 UDP 发送流程中最底层、最核心的实现函数。它的主要职责是:

  • 构建 UDP 头部:在数据缓冲区(pbuf)前添加 UDP 头,填充源端口、目的端口、长度。
  • 计算校验和 (Checksum):根据 IPv4/IPv6 协议要求,计算伪首部 + UDP 头 + 数据的校验和(除非用户显式禁用)
  • 处理特殊标志:处理组播回环(Multicast Loopback)、广播权限检查等。
  • 自动绑定端口:如果用户没有显式调用 bind(),在此处自动分配一个临时端口。
  • 调用 IP 层发送:调用 ip_output_if_src,将组装好的数据包交给 IP 层进行最终的路由封装和网卡驱动发送。
c 复制代码
err_t
udp_sendto_if_src(struct udp_pcb *pcb, struct pbuf *p,
                  const ip_addr_t *dst_ip, u16_t dst_port, struct netif *netif, const ip_addr_t *src_ip)
{
  struct udp_hdr *udphdr;
  err_t err;
  struct pbuf *q; /* q 是将要发送给 IP 层的完整数据包指针 */
  u8_t ip_proto;  /* IP 协议号 (UDP 或 UDPLITE) */
  u8_t ttl;       /* 生存时间 */
  ......
  
  /* 【1. 自动端口绑定】
   * 如果 PCB 还没有绑定本地端口 (local_port == 0),
   * 则在此处强制调用 udp_bind 分配一个临时端口。
   * 这是为了方便那些只调用 sendto 而没有先 bind 的应用程序。
   */
  if (pcb->local_port == 0) {
    LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE, ("udp_send: not yet bound to a port, binding now\n"));
    err = udp_bind(pcb, &pcb->local_ip, pcb->local_port);
    if (err != ERR_OK) {
      LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("udp_send: forced port bind failed\n"));
      return err;
    }
  }

  /* 【2. 内存空间检查与 UDP 头插入】
   * 尝试在现有的 pbuf (p) 头部腾出 UDP_HLEN (8字节) 的空间。
   */
  
  /* 检查长度相加是否会溢出 (安全性检查) */
  if ((u16_t)(p->tot_len + UDP_HLEN) < p->tot_len) {
    return ERR_MEM;
  }

  /* 尝试在现有 pbuf 头部增加空间 */
  if (pbuf_add_header(p, UDP_HLEN)) {
    /* 【情况 A: 现有 pbuf 头部空间不足】
     * 需要分配一个新的 pbuf (q) 专门存放 UDP 头。
     */
    q = pbuf_alloc(PBUF_IP, UDP_HLEN, PBUF_RAM);
    if (q == NULL) {
      LWIP_DEBUGF(UDP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("udp_send: could not allocate header\n"));
      return ERR_MEM;
    }
    
    /* 如果原始数据 p 不为空,将 p 链在 q 后面 (q -> p) */
    if (p->tot_len != 0) {
      pbuf_chain(q, p);
    }
    LWIP_DEBUGF(UDP_DEBUG, ("udp_send: added header pbuf %p before given pbuf %p\n", (void *)q, (void *)p));
  } else {
    /* 【情况 B: 成功在现有 pbuf 头部腾出空间】
     * 不需要新分配,q 直接指向 p。
     */
    q = p;
    LWIP_DEBUGF(UDP_DEBUG, ("udp_send: added header in given pbuf %p\n", (void *)p));
  }
  ......

  /* 【3. 填充 UDP 头部字段】
   * q->payload 现在指向 UDP 头的起始位置。
   */
  udphdr = (struct udp_hdr *)q->payload;
  udphdr->src = lwip_htons(pcb->local_port); /* 源端口 */
  udphdr->dest = lwip_htons(dst_port);       /* 目的端口 */
  udphdr->chksum = 0x0000;                   /* 初始化校验和为0,稍后计算 */
   ......

  /* 【4. 计算校验和 (Checksum Calculation)】
   * 区分 UDP-Lite 和普通 UDP。
   */
#if LWIP_UDPLITE
  if (pcb->flags & UDP_FLAGS_UDPLITE) {
	......
	
  } else
#endif /* LWIP_UDPLITE */
  {
    /* --- 普通 UDP 逻辑 --- */
    LWIP_DEBUGF(UDP_DEBUG, ("udp_send: UDP packet length %"U16_F"\n", q->tot_len));
    
    /* UDP 头部 len 字段 = 包总长度 */
    udphdr->len = lwip_htons(q->tot_len);

    /* 执行校验和计算 */
#if CHECKSUM_GEN_UDP
    IF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_UDP) {
      /* IPv6 强制要求校验和,或者 IPv4 中未显式禁用校验和 */
      if (IP_IS_V6(dst_ip) || (pcb->flags & UDP_FLAGS_NOCHKSUM) == 0) {
        u16_t udpchksum;
        ......
        {
          /* 标准路径:计算整个包的校验和 */
          udpchksum = ip_chksum_pseudo(q, IP_PROTO_UDP, q->tot_len,
                                       src_ip, dst_ip);
        }

        /* 修正:结果 0 变为 0xffff */
        if (udpchksum == 0x0000) {
          udpchksum = 0xffff;
        }
        udphdr->chksum = udpchksum;
      }
    }
#endif
    ip_proto = IP_PROTO_UDP;
  }

  /* 【5. 确定 TTL (Time To Live)】
 * 如果是组播包,使用专门的组播 TTL;否则使用默认 TTL。
   */
#if LWIP_MULTICAST_TX_OPTIONS
  ttl = (ip_addr_ismulticast(dst_ip) ? udp_get_multicast_ttl(pcb) : pcb->ttl);
#else
  ttl = pcb->ttl;
#endif
   ......

  /* 【6. 调用 IP 层发送】
   * 设置网卡 hints (用于优化,如提示单播/组播),然后调用 ip_output_if_src。
   * 这一步会将 IP 头添加到 q 前面,并最终调用网卡驱动发送。
   */
  NETIF_SET_HINTS(netif, &(pcb->netif_hints));
  err = ip_output_if_src(q, src_ip, dst_ip, ttl, pcb->tos, ip_proto, netif);
  NETIF_RESET_HINTS(netif);

  /* 【7. 统计信息更新】
 * 增加 UDP 发出数据包计数。
   */
  MIB2_STATS_INC(mib2.udpoutdatagrams);

  if (q != p) {
    /* 释放临时分配的头部 pbuf */
    pbuf_free(q);
    q = NULL;
    /* p 仍然由调用者引用,将继续存活 (Caller 负责后续释放 p) */
  }

  /* 增加发送统计 */
  UDP_STATS_INC(udp.xmit);
  
  return err;
}

2.3 网络层(IP)

ip_output_if_src

它的核心职责包括:

  • 构建/完善 IP 头部:如果上层(如 UDP/TCP)还没加 IP 头,它负责在 pbuf 最前面插入 IP 头,并填充版本、长度、TTL、协议号、源/目的 IP 等字段
  • 计算 IP 校验和:根据配置(软件计算或硬件卸载),计算 IP 头部的校验和。
  • 处理特殊选项:支持 IP Options(如记录路由、时间戳等,需编译开启)。
  • 本地回环 (Loopback):如果目的 IP 是本机,直接将包放入接收队列,不经过物理网卡。
  • 分片 (Fragmentation):如果数据包大小超过了网卡的 MTU,且允许分片,则调用 ip4_frag 进行分片。
  • 最终交付:调用网卡驱动层的 netif->output 函数,将数据帧发送给链路层。
c 复制代码
需要注意的是,netif->mtu 一般由网络协议栈在初始化阶段设置,它表示 IP 层允许发送的最大数据包长度(不包含链路层头部)。
在以太网中,常见的 MTU 值为 1500 字节。

在很多网卡控制器中,硬件还会提供 "最大发送帧长度(TX Max Frame Length)" 和 "最大接收帧长度(RX Max Frame Length)" 的配置,
用于限制网卡能够处理的最大以太网帧大小。

因此在系统设计时需要保证:

netif->mtu + Ethernet Header + FCS ≤ 网卡允许的最大帧长度

其中:

 - Ethernet Header:14 字节
 - FCS:4 字节

如果该条件不满足,当发送或接收的以太网帧超过网卡硬件限制时,数据包可能会被网卡直接丢弃,或者产生发送/接收错误。
c 复制代码
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)
{
  struct ip_hdr *iphdr;
  ip4_addr_t dest_addr;
  ......

  /* 【1. 判断是否需要生成 IP 头部】
   * 如果 dest != LWIP_IP_HDRINCL,说明上层没加 IP 头,我们需要自己生成。
   * 如果 dest == LWIP_IP_HDRINCL,说明用户使用了原始套接字 (Raw Socket),IP 头已包含在 p 中。
   */
  if (dest != LWIP_IP_HDRINCL) {
    u16_t ip_hlen = IP_HLEN; /* 默认 IP 头长度 20 字节 */
    
#if IP_OPTIONS_SEND
    /* --- 处理 IP 选项 (IP Options) --- */
    u16_t optlen_aligned = 0;
    if (optlen != 0) {
      /* 检查选项长度是否合法 (不能超过最大头长度 60 - 20 = 40 字节) */
      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;
      }
      
      /* IP 选项长度必须是 4 字节的倍数,向上取整 */
      optlen_aligned = (u16_t)((optlen + 3) & ~3);
      ip_hlen += optlen_aligned; /* 更新总头长度 */

      /* 在 pbuf 头部腾出选项空间 */
      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;
      }
      
      /* 拷贝选项数据到 pbuf 头部 */
      MEMCPY(p->payload, ip_options, optlen);
      /* 如果有填充字节,清零 */
      if (optlen < optlen_aligned) {
        memset(((char *)p->payload) + optlen, 0, (size_t)(optlen_aligned - optlen));
      }
      ......
    }
#endif /* IP_OPTIONS_SEND */

    /* --- 生成标准 IP 头部 --- */
    /* 在 pbuf 头部腾出 IP 头空间 (通常 20 字节) */
    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;
    }

    /* 获取 IP 头指针 */
    iphdr = (struct ip_hdr *)p->payload;
    LWIP_ASSERT("check that first pbuf can hold struct ip_hdr",
                (p->len >= sizeof(struct ip_hdr)));

    /* 【填充 IP 头字段】 */
    IPH_TTL_SET(iphdr, ttl);       /* 设置 TTL */
    IPH_PROTO_SET(iphdr, proto);   /* 设置协议号 (UDP/TCP等) */
    ......

    /* 设置目的 IP */
    ip4_addr_copy(iphdr->dest, *dest);
    ......

    /* 设置版本 (4) 和 头部长度 (IHL) */
    IPH_VHL_SET(iphdr, 4, ip_hlen / 4);
    ......

    /* 设置总长度 (Total Length) */
    IPH_LEN_SET(iphdr, lwip_htons(p->tot_len));

    /* 设置分片偏移 (这里设为 0,表示不分片,分片逻辑在后面) */
    IPH_OFFSET_SET(iphdr, 0);
    
    /* 设置标识符 (ID),用于分片重组 */
    IPH_ID_SET(iphdr, lwip_htons(ip_id));

    ++ip_id; /* 全局 ID 自增 */

    /* 设置源 IP */
    if (src == NULL) {
      /* 如果源 IP 为空,使用 0.0.0.0 (通常在后续逻辑会被替换为网卡 IP,但这里先填任意) */
      ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);
    } else {
      ip4_addr_copy(iphdr->src, *src);
    }

    /* 【计算 IP 校验和】 */
#if CHECKSUM_GEN_IP_INLINE
   ......
#else /* 标准计算方式 */
    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
#endif
  } else {
    /* 【原始套接字模式 (HDRINCL)】
     * IP 头已存在,只需提取目的地址用于后续路由判断。
     */
    ......
    iphdr = (struct ip_hdr *)p->payload;
    ip4_addr_copy(dest_addr, iphdr->dest);
    dest = &dest_addr; /* 更新 dest 指针指向包内的目的 IP */
  }
  ......

  IP_STATS_INC(ip.xmit); /* 统计成功发出的 IP 包 */
  ......

  /* 【2. 本地回环处理 (Loopback)】
   * 如果目的 IP 是本机 IP,或者是一个回环地址,不需要发到物理网线,直接内部排队接收。
   */
#if ENABLE_LOOPBACK
  if (ip4_addr_cmp(dest, netif_ip4_addr(netif))
#if !LWIP_HAVE_LOOPIF
      || ip4_addr_isloopback(dest)
#endif
     ) {
    LWIP_DEBUGF(IP_DEBUG, ("netif_loop_output()"));
    return netif_loop_output(netif, p); /* 放入本机接收队列 */
  }
  ......
  
#endif /* ENABLE_LOOPBACK */

  /* 【3. IP 分片处理 (Fragmentation)】
   * 如果数据包总长 > 网卡 MTU,且网卡 MTU 不为 0 (非回环口),则需要分片。
   */
#if IP_FRAG
  if (netif->mtu && (p->tot_len > netif->mtu)) {
    return ip4_frag(p, netif, dest); /* 执行分片发送 */
  }
#endif

  /* 【4. 最终发送】
   * 调用网卡驱动层的 output 函数。
   * 对于以太网,这通常是 etharp_output -> ethernet_output,负责添加以太网头并 DMA 发送。
   */
  LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));
  return netif->output(netif, p, dest);
}

netif->output

常见的,在 netdev_netif_init 初始化 netif 时,会给 netif->output 赋值。我们这里以常见的 NETDEV_TYPE_ETHERNET 为例。

c 复制代码
/* lwip netif add call back function */
static err_t  netdev_netif_init (struct netif *netif)
{
......
  switch (netdev->net_type) {
  
  case NETDEV_TYPE_ETHERNET:
    MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd, (u32_t)netdev->speed);
    netif->flags = NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET;
    netif->output = etharp_output;
#if LWIP_IPV6
    netif->output_ip6 = ethip6_output;
#endif /* LWIP_IPV6 */
    netif->ar_hrd = ARPHRD_ETHER;
    break;
......
}

etharp_output 是 LwIP 协议栈中 IPv4 到 以太网链路层 (Ethernet) 的"翻译官"和"导航员"。

它的核心任务非常明确:已知目标 IP 地址,找到对应的目标 MAC 地址,然后调用底层驱动发送数据

在以太网中,数据包不能只凭 IP 地址发送,必须封装成以太网帧,而以太网帧头需要目的 MAC 地址。这个函数就是解决"IP -> MAC"映射问题的关键入口。

函数的逻辑可以分为三条主要路径:

  • 广播 (Broadcast):直接发给全 FF:FF:FF:FF:FF:FF。
  • 组播 (Multicast):通过算法将 IP 映射为特定的组播 MAC。
  • 单播 (Unicast):
    • 查路由:如果目标不在同一网段,MAC 地址应该是网关 (Gateway) 的 MAC,而不是目标的 MAC。
    • 查 ARP 缓存:在 ARP 表中查找该 IP(或网关 IP)对应的 MAC。
      • 命中 (Hit):直接发送。
      • 未命中 (Miss):发起 ARP 请求(询问"谁是这个 IP?"),并将当前数据包排队,等回复后再发
c 复制代码
err_t
etharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr)
{
  const struct eth_addr *dest;       // 最终确定的目标 MAC 地址指针
  struct eth_addr mcastaddr;         // 临时存储计算出的组播 MAC
  const ip4_addr_t *dst_addr = ipaddr; // 实际需要查询 ARP 的 IP 地址 (可能是网关 IP)
  ......
  
  /* 情况 A: 目标是广播地址 (如 192.168.1.255) */
  if (ip4_addr_isbroadcast(ipaddr, netif)) {
     ......
    
  /* 情况 B: 目标是组播地址 (224.0.0.0 - 239.255.255.255) */
  } else if (ip4_addr_ismulticast(ipaddr)) {
     ......
    
  /* 情况 C: 目标是单播地址 (普通通信) */
  } else {
    netif_addr_idx_t i;
    
    /* --- 路由判断:目标是否在同一局域网? --- */
    /* 如果目标 IP 与本机 IP 不在同一子网 (netcmp 失败),且不是链路本地地址 */
    if (!ip4_addr_netcmp(ipaddr, netif_ip4_addr(netif), netif_ip4_netmask(netif)) &&
        !ip4_addr_islinklocal(ipaddr)) {
      
#if LWIP_AUTOIP
      /* AutoIP 特殊处理:链路本地地址不能经过路由器转发 */
      struct ip_hdr *iphdr = LWIP_ALIGNMENT_CAST(struct ip_hdr *, q->payload);
      if (!ip4_addr_islinklocal(&iphdr->src))
#endif /* LWIP_AUTOIP */
      {
#ifdef LWIP_HOOK_ETHARP_GET_GW
        /* 高级路由钩子:如果有多个网关,动态选择下一个跳的 IP */
        dst_addr = LWIP_HOOK_ETHARP_GET_GW(netif, ipaddr);
        if (dst_addr == NULL)
#endif /* LWIP_HOOK_ETHARP_GET_GW */
        {
          /* 检查是否配置了默认网关 */
          if (!ip4_addr_isany_val(*netif_ip4_gw(netif))) {
            /* 关键点:如果目标在外网,我们需要把包发给【网关】,而不是目标本身 */
            /* 所以这里将 dst_addr 指向网关 IP,后续查 ARP 也是查网关的 MAC */
            dst_addr = netif_ip4_gw(netif);
          } else {
            /* 没有网关,无法路由到外网 */
            return ERR_RTE;
          }
        }
      }
    }
    
    /* --- 查 ARP 缓存表 (加速路径) --- */
    
    /* 优化 1: 使用 PCB 缓存的提示 (Hint) */
    /* 如果上次在这个连接上成功发送过,LwIP 会记住 ARP 表索引,避免遍历 */
#if LWIP_NETIF_HWADDRHINT
    if (netif->hints != NULL) {
      netif_addr_idx_t etharp_cached_entry = netif->hints->addr_hint;
      if (etharp_cached_entry < ARP_TABLE_SIZE) {
#endif /* LWIP_NETIF_HWADDRHINT */
        /* 检查缓存项是否有效:状态稳定 + 属于本网口 + IP 匹配 */
        if ((arp_table[etharp_cached_entry].state >= ETHARP_STATE_STABLE) &&
#if ETHARP_TABLE_MATCH_NETIF
            (arp_table[etharp_cached_entry].netif == netif) &&
#endif
            (ip4_addr_cmp(dst_addr, &arp_table[etharp_cached_entry].ipaddr))) {
          
          ETHARP_STATS_INC(etharp.cachehit); /* 统计缓存命中 */
          /* 直接使用缓存的索引发送 */
          return etharp_output_to_arp_index(netif, q, etharp_cached_entry);
        }
#if LWIP_NETIF_HWADDRHINT
      }
    }
#endif /* LWIP_NETIF_HWADDRHINT */

    /* 优化 2: 遍历 ARP 表查找稳定条目 */
    /* 这是一个 O(N) 操作,但在嵌入式系统中 ARP 表通常很小 (5-10 项) */
    for (i = 0; i < ARP_TABLE_SIZE; i++) {
      if ((arp_table[i].state >= ETHARP_STATE_STABLE) &&
#if ETHARP_TABLE_MATCH_NETIF
          (arp_table[i].netif == netif) &&
#endif
          (ip4_addr_cmp(dst_addr, &arp_table[i].ipaddr))) {
        
        /* 找到了!更新 Hint 以便下次加速 */
        ETHARP_SET_ADDRHINT(netif, i);
        /* 直接发送 */
        return etharp_output_to_arp_index(netif, q, i);
      }
    }
    
    /* --- 缓存未命中 (Miss) --- */
    /* 没有对应的 MAC 地址,需要发起 ARP 请求,并将数据包排队 */
    /* etharp_query 会做两件事:
     * 1. 发送 ARP Request ("Who has dst_addr? Tell me.")
     * 2. 将当前数据包 q 挂起到等待队列,等 ARP Reply 回来后再发(不会阻塞)
     */
    return etharp_query(netif, dst_addr, q);
  }
  
  /* 调用底层以太网输出函数 */
  /* 参数:接口,数据包,源 MAC(本机), 目的 MAC(刚才计算的), 类型 (IPv4=0x0800) */
  return ethernet_output(netif, q, (struct eth_addr *)(netif->hwaddr), dest, ETHTYPE_IP);
}

2.4 数据链路层

ethernet_output

这个函数 ethernet_output 是 LwIP 协议栈中 数据发送流程的"最后一站"。

它的任务非常简单但至关重要:给数据包穿上"以太网外衣"(添加以太网帧头),然后直接交给网卡驱动(硬件)去发送

在此之前,数据只是 IP 包(网络层);在此之后,数据变成了可以在网线/WiFi 上传输的以太网帧(链路层)。核心功能如下:

  • 添加以太网头部 (Ethernet Header):
    • 在 pbuf 的最前面腾出 14 字节 空间(标准以太网头长度)。
  • 填充三个关键字段:
    • 目的 MAC (dest):发给谁?
    • 源 MAC (src):我是谁?
    • 类型 (type):里面装的是什么数据?(通常是 0x0800 代表 IPv4,0x0806 代表 ARP)。
  • 调用驱动发送:
    • 调用 netif->linkoutput。这是网卡驱动层必须实现的函数,负责把数据搬移到硬件 DMA 缓冲区并触发发送中断。
c 复制代码
err_t
ethernet_output(struct netif * netif, struct pbuf * p,
                const struct eth_addr * src, const struct eth_addr * dst,
                u16_t eth_type) {
  struct eth_hdr *ethhdr;
  /* 将主机字节序的类型转换为网络字节序 (大端序) */
  u16_t eth_type_be = lwip_htons(eth_type);
  ......
  {
    /* 【普通模式:只添加标准以太网头】 */
    /* 在 pbuf 头部腾出 14 字节空间 */
    if (pbuf_add_header(p, SIZEOF_ETH_HDR) != 0) {
      goto pbuf_header_failed;
    }
  }

  /* 获取以太网头指针 (现在 p->payload 指向刚腾出的头部空间) */
  ethhdr = (struct eth_hdr *)p->payload;
  
  /* 【1. 填充以太网头字段】 */
  ethhdr->type = eth_type_be;          /* 设置类型 (IPv4/ARP/VLAN) */
  
  /* 拷贝目的 MAC 地址 (6 字节) */
  SMEMCPY(&ethhdr->dest, dst, ETH_HWADDR_LEN);
  
  /* 拷贝源 MAC 地址 (6 字节) */
  SMEMCPY(&ethhdr->src,  src, ETH_HWADDR_LEN);

  /*  sanity check: 确保网卡硬件地址长度是 6 (以太网标准) */
  LWIP_ASSERT("netif->hwaddr_len must be 6 for ethernet_output!",
              (netif->hwaddr_len == ETH_HWADDR_LEN));        
  .....


  /* 【2. 最终发送:调用网卡驱动】 */
  return netif->linkoutput(netif, p);

/* 错误处理:如果 pbuf 头部空间不足 */
pbuf_header_failed:
  LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS,
              ("ethernet_output: could not allocate room for header.\n"));
  LINK_STATS_INC(link.lenerr); /* 统计链路层长度错误 */
  return ERR_BUF;
}

3、总结

相关推荐
云边散步1 小时前
godot2D游戏教程系列二(15)
笔记·学习·游戏·游戏开发
H Journey1 小时前
学习OpenCV之HSV 颜色模式
人工智能·opencv·学习·hsv
菜菜小狗的学习笔记1 小时前
黑马程序员java web学习笔记--项目部署(Linux)
笔记·学习
charlie1145141911 小时前
通用GUI编程技术——Win32 原生编程实战(五)——ListView 控件详解
windows·学习·gui·win32·编程指南
承渊政道2 小时前
C++学习之旅【智能指针的使⽤及其原理】
开发语言·c++·笔记·vscode·学习·visual studio
承渊政道2 小时前
C++学习之旅【异常相关内容以及类型转换介绍】
c语言·c++·笔记·vscode·学习·macos·visual studio
飞Link2 小时前
洞察数据的“分寸感”:深度解析对比学习(Contrastive Learning)
开发语言·python·学习·数据挖掘
不知名。。。。。。。。2 小时前
仿muduo库实现高并发服务器----通信链接管理Connection
运维·服务器·网络
承渊政道2 小时前
C++学习之旅【深入回溯C++11的发展历程】
c语言·c++·笔记·vscode·学习·macos·visual studio