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 指针)
- 情况1:调用 LWIP 协议中接口
- 调用
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(ðhdr->dest, dst, ETH_HWADDR_LEN);
/* 拷贝源 MAC 地址 (6 字节) */
SMEMCPY(ðhdr->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、总结
