LwIP入门实战 — 4 LwIP的网络接口管理
目录
4.1 netif结构体
4.2 netif使用
4.3 协议栈与网络接口初始化
4.4 发送流程
4.5 接受流程
4.1 netif结构体
netif
结构体是 lwIP抽象和管理网络接口的核心数据结构,其设计初衷是为了解决不同网络硬件(如以太网、Wi-Fi、LoRa 等)与协议栈的适配问题,通过统一的接口封装实现硬件与协议栈的解耦。
netif
结构体整合了网络接口的关键信息:包括硬件标识(MAC 地址、接口名称)、网络配置(IP 地址、子网掩码、网关)、数据处理函数(接收数据的input
、发送数据的output
等回调)、运行状态(是否启用、链路是否连接等标志)及扩展字段(硬件私有数据指针、状态变化回调)。使用netif
结构体,协议栈无需关心底层硬件细节,可通过统一的方法管理各类网络接口,简化了多接口场景下的开发复杂度,同时让硬件驱动专注于具体的收发实现,大幅提升了网络模块的可移植性和扩展性。
struct netif {// 链表节点:用于将多个网络接口连接成链表struct netif *next;// 网络接口名称(通常为2个字符,如"en"表示以太网)const char *name;// 接口索引(用于区分同一类型的多个接口,如en0、en1)uint8_t num;// 网络层地址(IP地址、子网掩码、网关)ip_addr_t ip_addr; // 接口IP地址ip_addr_t netmask; // 子网掩码ip_addr_t gw; // 网关地址// 硬件(链路层)相关操作函数netif_input_fn input; // 从链路层接收数据后,提交到网络层的回调函数netif_output_fn output; // 从网络层发送数据到链路层的函数(直接发送)netif_linkoutput_fn linkoutput; // 用于ARP协议的链路层发送函数// 接口状态标志(通过位运算组合)uint8_t flags; // 如NETIF_FLAG_UP(接口已启用)、NETIF_FLAG_LINK_UP(链路已连接)等// 链路层地址信息(通常为MAC地址)struct eth_addr hwaddr; // 硬件地址(6字节MAC地址)uint8_t hwaddr_len; // 硬件地址长度(以太网为6字节)// 接口最大传输单元(MTU),以太网通常为1500字节uint16_t mtu;// 接口类型(如NETIF_TYPE_ETHERNET、NETIF_TYPE_WIFI等)uint8_t type;// 统计信息(可选,用于流量监控)struct netif_stats stats; // 包含发送/接收的字节数、数据包数、错误数等// 用户自定义数据指针(可扩展接口功能,如绑定硬件私有数据)void *state;// 链路状态变化回调函数(如链路连接/断开时触发)netif_status_callback_fn status_callback;// 地址变化回调函数(如IP地址修改时触发)netif_status_callback_fn link_callback;// 多播相关配置(用于组播功能)struct ip_mreq multicast_mac_filter[NETIF_MAX_MULTICAST_FILTERS];uint16_t multicast_mac_filter_cnt;
};
为什么是在IP 层进行分片处理?
因为链路层不提供任何的差错处理机制,如果在网卡中接收的数据包不满足网卡自身的属性,那么网卡可能就会直接丢弃该数据包,也可能在底层进行分包发送,但是这种分包在 IP 层看来是不可接受的,因为它打乱了数据的结构,所以只能由 IP层进行分片处理。
4.2 netif使用
那么 netif 具体该如何使用呢?其实使用还是非常简单的。首先我们需要根据我们的网卡定义一个netif 结构体变量 struct netif gnetif,我们首先要把网卡挂载到netif_list 链表上才能使用,因为LwIP 是通过链表来管理所有的网卡,所有第一步是通过netif_add()函数将我们的网卡挂载到netif_list 链表上,netif_add()函数具体见代码清单。
/*** @ingroup netif* 向lwIP的网络接口列表中添加一个网络接口** @param netif 预分配的网络接口结构体* @param ipaddr 新接口的IP地址* @param netmask 新接口的子网掩码* @param gw 新接口的默认网关IP地址* @param state 传递给新接口的不透明数据(驱动私有数据)* @param init 用于初始化接口的回调函数* @param input 用于将入站数据包上传到协议栈的回调函数** @return 成功返回netif,失败返回NULL*/
struct netif *
netif_add(struct netif *netif,
#if LWIP_IPV4const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,
#endif /* LWIP_IPV4 */void *state, netif_init_fn init, netif_input_fn input)
{
#if LWIP_IPV6s8_t i; // 用于IPv6地址数组的循环索引
#endif// 断言检查核心锁是否已获取(确保线程安全)LWIP_ASSERT_CORE_LOCKED();#if LWIP_SINGLE_NETIF// 如果配置为单网络接口模式,检查是否已存在默认接口if (netif_default != NULL) {LWIP_ASSERT("single netif already set", 0); // 断言失败,单接口已被设置return NULL;}
#endif// 错误检查:网络接口结构体不能为空LWIP_ERROR("netif_add: invalid netif", netif != NULL, return NULL);// 错误检查:初始化回调函数不能为空LWIP_ERROR("netif_add: No init function given", init != NULL, return NULL);#if LWIP_IPV4// 如果未提供IP地址相关参数,使用默认的"任意地址"if (ipaddr == NULL) {ipaddr = ip_2_ip4(IP4_ADDR_ANY);}if (netmask == NULL) {netmask = ip_2_ip4(IP4_ADDR_ANY);}if (gw == NULL) {gw = ip_2_ip4(IP4_ADDR_ANY);}/* 重置新接口的配置状态 */// 初始化IPv4地址相关字段为0ip_addr_set_zero_ip4(&netif->ip_addr);ip_addr_set_zero_ip4(&netif->netmask);ip_addr_set_zero_ip4(&netif->gw);// 设置默认的IPv4输出函数(空实现)netif->output = netif_null_output_ip4;
#endif /* LWIP_IPV4 */#if LWIP_IPV6// 初始化IPv6地址数组for (i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) {ip_addr_set_zero_ip6(&netif->ip6_addr[i]); // 地址清零netif->ip6_addr_state[i] = IP6_ADDR_INVALID; // 标记地址为无效
#if LWIP_IPV6_ADDRESS_LIFETIMES// 设置地址生存时间为静态(默认)netif->ip6_addr_valid_life[i] = IP6_ADDR_LIFE_STATIC;netif->ip6_addr_pref_life[i] = IP6_ADDR_LIFE_STATIC;
#endif /* LWIP_IPV6_ADDRESS_LIFETIMES */}// 设置默认的IPv6输出函数(空实现)netif->output_ip6 = netif_null_output_ip6;
#endif /* LWIP_IPV6 */// 启用所有类型的校验和检查NETIF_SET_CHECKSUM_CTRL(netif, NETIF_CHECKSUM_ENABLE_ALL);netif->mtu = 0; // 初始化MTU(最大传输单元)为0,由驱动后续设置netif->flags = 0; // 初始化接口标志为0#ifdef netif_get_client_data// 初始化客户端数据区(清零)memset(netif->client_data, 0, sizeof(netif->client_data));
#endif /* LWIP_NUM_NETIF_CLIENT_DATA */#if LWIP_IPV6
#if LWIP_IPV6_AUTOCONFIG// 默认禁用IPv6地址自动配置netif->ip6_autoconfig_enabled = 0;
#endif /* LWIP_IPV6_AUTOCONFIG */// 重置该接口的IPv6邻居发现协议状态nd6_restart_netif(netif);
#endif /* LWIP_IPV6 */#if LWIP_NETIF_STATUS_CALLBACK// 初始化状态回调函数为NULLnetif->status_callback = NULL;
#endif /* LWIP_NETIF_STATUS_CALLBACK */#if LWIP_NETIF_LINK_CALLBACK// 初始化链路回调函数为NULLnetif->link_callback = NULL;
#endif /* LWIP_NETIF_LINK_CALLBACK */#if LWIP_IGMP// 初始化IGMP(互联网组管理协议)的MAC过滤函数为NULLnetif->igmp_mac_filter = NULL;
#endif /* LWIP_IGMP */#if LWIP_IPV6 && LWIP_IPV6_MLD// 初始化MLD(多播监听发现)的MAC过滤函数为NULLnetif->mld_mac_filter = NULL;
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */#if ENABLE_LOOPBACK// 初始化环回数据包队列的首尾指针netif->loop_first = NULL;netif->loop_last = NULL;
#endif /* ENABLE_LOOPBACK *//* 保存网络接口的特定状态信息 */netif->state = state; // 保存驱动私有数据netif->num = netif_num; // 分配临时的接口编号netif->input = input; // 设置输入数据包处理函数// 重置网络接口的提示信息(用于路径MTU发现等)NETIF_RESET_HINTS(netif);#if ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS// 初始化环回数据包计数netif->loop_cnt_current = 0;
#endif /* ENABLE_LOOPBACK && LWIP_LOOPBACK_MAX_PBUFS */#if LWIP_IPV4// 设置IPv4地址、子网掩码和网关netif_set_addr(netif, ipaddr, netmask, gw);
#endif /* LWIP_IPV4 *//* 调用用户指定的网络接口初始化函数 */if (init(netif) != ERR_OK) {return NULL; // 初始化失败,返回NULL}#if LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES// 初始化IPv6的MTU为驱动设置的值(可通过路由通告更新)netif->mtu6 = netif->mtu;
#endif /* LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES */#if !LWIP_SINGLE_NETIF/* 分配一个唯一的接口编号(范围0..254),使(num+1)可以作为u8_t类型的接口索引假设新接口尚未添加到列表中此算法为O(n^2),但对于lwIP来说足够高效*/{struct netif *netif2;int num_netifs;do {// 编号循环(0-254)if (netif->num == 255) {netif->num = 0;}num_netifs = 0;// 检查当前编号是否已被其他接口使用for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) {LWIP_ASSERT("netif already added", netif2 != netif); // 确保未重复添加num_netifs++;// 断言检查接口数量不超过255(编号限制)LWIP_ASSERT("too many netifs, max. supported number is 255", num_netifs <= 255);// 如果编号已存在,递增编号并重新检查if (netif2->num == netif->num) {netif->num++;break;}}} while (netif2 != NULL); // 直到找到未使用的编号}// 更新下一个可用的接口编号if (netif->num == 254) {netif_num = 0;} else {netif_num = (u8_t)(netif->num + 1);}/* 将此接口添加到接口列表 */netif->next = netif_list; // 新接口的next指向当前列表头部netif_list = netif; // 列表头部更新为新接口
#endif /* !LWIP_SINGLE_NETIF */// 通知MIB2(管理信息库)添加了新接口mib2_netif_added(netif);#if LWIP_IGMP/* 启动IGMP处理(如果接口启用了IGMP标志) */if (netif->flags & NETIF_FLAG_IGMP) {igmp_start(netif);}
#endif /* LWIP_IGMP */// 调试信息:打印添加的接口信息LWIP_DEBUGF(NETIF_DEBUG, ("netif: added interface %c%c IP",netif->name[0], netif->name[1]));
#if LWIP_IPV4LWIP_DEBUGF(NETIF_DEBUG, (" addr "));ip4_addr_debug_print(NETIF_DEBUG, ipaddr);LWIP_DEBUGF(NETIF_DEBUG, (" netmask "));ip4_addr_debug_print(NETIF_DEBUG, netmask);LWIP_DEBUGF(NETIF_DEBUG, (" gw "));ip4_addr_debug_print(NETIF_DEBUG, gw);
#endif /* LWIP_IPV4 */LWIP_DEBUGF(NETIF_DEBUG, ("\n"));// 调用扩展回调函数,通知接口已添加netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);return netif; // 返回添加的网络接口
}
挂载网卡代码如下:
// 定义IP地址的各个字节
#define IP_ADDR0 192
#define IP_ADDR1 168
#define IP_ADDR2 1
#define IP_ADDR3 122 // 定义子网掩码的各个字节
#define NETMASK_ADDR0 255
#define NETMASK_ADDR1 255
#define NETMASK_ADDR2 255
#define NETMASK_ADDR3 0 // 定义网关地址的各个字节
#define GW_ADDR0 192
#define GW_ADDR1 168
#define GW_ADDR2 1
#define GW_ADDR3 1 // 声明一个netif结构体变量,用于表示网络接口
struct netif gnetif; // 声明三个ip4_addr_t类型的变量,分别用于存储IP地址、子网掩码和网关地址
ip4_addr_t ipaddr;
ip4_addr_t netmask;
ip4_addr_t gw; // 声明三个uint8_t类型的数组,但在提供的代码段中未使用
uint8_t IP_ADDRESS[4];
uint8_t NETMASK_ADDRESS[4];
uint8_t GATEWAY_ADDRESS[4]; // TCPIP_Init函数,用于初始化TCP/IP网络
void TCPIP_Init(void)
{ // 初始化LwIP栈 tcpip_init(NULL, NULL); // 根据是否定义了USE_DHCP宏来设置IP地址、子网掩码和网关 #ifdef USE_DHCP // 如果定义了USE_DHCP,则将IP地址、子网掩码和网关设置为零地址,以使用DHCP自动获取 ip_addr_set_zero_ip4(&ipaddr); ip_addr_set_zero_ip4(&netmask); ip_addr_set_zero_ip4(&gw); #else // 如果没有定义USE_DHCP,则使用宏定义的地址手动设置IP地址、子网掩码和网关 IP4_ADDR(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3); IP4_ADDR(&netmask, NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3); IP4_ADDR(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3); #endif /* USE_DHCP */ // 将网络接口添加到LwIP栈中,并配置其IP地址、子网掩码、网关等 netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input); // 将当前网络接口设置为默认网络接口 netif_set_default(&gnetif); // 检查网络接口链路状态 if (netif_is_link_up(&gnetif)) { // 如果链路已建立,则将网络接口设置为“活动”状态 netif_set_up(&gnetif); } else { // 如果链路未建立,则将网络接口设置为“非活动”状态 netif_set_down(&gnetif); }
}
挂载网卡的过程是非常简单的,如果一个设备当前是还没有网卡的,当调用 netif_add()函数挂载网卡后,其过程如图所示,当设备需要挂载多个网卡的时候,就多次调用netif_add()函数即可,新挂载的网卡会在链表的最前面。
图 挂载网卡
lwip为什么使用链表而不使用数组管理netif?
链表支持运行时动态增删接口、无需预分配固定内存、不依赖连续地址空间,且接口数量通常极少(1~5个),O(n)遍历开销可忽略;而数组需要预先确定大小、浪费内存或限制扩展、增删效率低
4.3 协议栈与网络接口初始化
初始化是协议栈工作的前提,主要完成 lwIP 内核初始化、网络接口(如以太网)注册与硬件初始化。核心函数调用流程:
系统启动 → lwip_init() → netif_add() → ethernetif_init() → low_level_init()
lwip_init():初始化 lwIP 协议栈内核,是所有操作的起点。用于初始化内存池(pbuf
、tcp_pcb
等)、协议栈核心模块(IP、ARP、UDP、TCP 等)、网络接口列表(netif_list
)等。
netif_add():向协议栈注册一个网络接口(如以太网、WiFi),将其加入全局接口列表(netif_list
)。需传入接口初始化回调(ethernetif_init
)、数据包输入回调(tcpip_input
或 netif_input
)等。
ethernetif_init():以太网接口的初始化入口(驱动与协议栈的对接层),由 netif_add()
调用。用于配置 netif
结构体(如接口名称、MTU、支持的标志 NETIF_FLAG_ETHARP
(支持 ARP)、NETIF_FLAG_BROADCAST
(支持广播)等);绑定链路层发送函数(netif->linkoutput = low_level_output
)。并调用硬件底层初始化函数 low_level_init()
。
low_level_init():直接操作硬件的底层初始化(用户需根据硬件实现)。初始化以太网 MAC 控制器(如时钟、寄存器配置);设置本地 MAC 地址(写入 netif->hwaddr
);配置硬件中断(接收 / 发送中断);使能硬件的接收 / 发送功能。
netif_set_default()(可选):将某个网络接口设为默认接口(用于无特定路由的数据包发送)。
4.4 发送流程
发送流程是数据从应用层(如 UDP/TCP 应用)经过协议栈逐层处理,最终通过硬件发送到物理链路的过程。以 UDP 发送 IPv4 数据包 为例,核心流程如下:
应用层 sendto() → udp_send() → ip_output() → etharp_output() → low_level_output() → 硬件发送
应用层sendto():应用层发送数据的入口(用户调用),传入目标 IP、端口和数据。通常是对 lwIP 提供的 udp_sendto()
等函数的封装。
传输层udp_send() / udp_sendto():UDP 协议处理,封装 UDP 头部(源端口、目标端口、长度、校验和)。进一步将封装后的数据(含 UDP 头部)传递给网络层的 ip_output()
。
网络层ip_output():IP 协议处理,封装 IP 头部(版本、长度、TTL、协议类型、源 / 目标 IP 等)。用于选择输出接口(通过路由表或默认接口);若数据包超过 MTU,进行分片(IP 层分片);进一步调用链路层发送函数(netif->output
,通常绑定为 etharp_output
)。
链路层etharp_output():处理以太网链路层逻辑,通过 ARP 协议获取目标 IP 对应的 MAC 地址。若 ARP 缓存中有目标 MAC,直接封装以太网帧头;若 ARP 缓存中无目标 MAC,发送 ARP 请求,待收到回复后重发数据。进一步调用硬件发送函数(netif->linkoutput
,即 low_level_output
)。
硬件驱动层low_level_output():将协议栈传递的 pbuf
数据通过硬件发送。从 pbuf
链式结构中提取数据拼接成连续的以太网帧,并将数据写入硬件发送缓冲区进行发送。
4.5 接受流程
接收流程是硬件收到数据后,经协议栈逐层解析,最终传递到应用层的过程。以 以太网接收 IPv4 UDP 数据包 为例,核心流程如下:
硬件接收中断 → ethernetif_input() → low_level_input() → netif_input() → ip_input() → udp_input() → 应用层回调
硬件层:硬件(如以太网控制器)检测到新数据帧到达接收缓冲区时,会自动触发中断,这是数据接收的起点。中断服务程序(ISR)随之响应,通过置位接收标志在软件层面标记有新数据待处理,并清除硬件中断标志以避免中断重复触发,之后通常会调用ethernetif_input()
函数,将数据处理交接给驱动与协议栈对接层。
驱动与协议栈对接层ethernetif_input():ethernetif_input()
作为以太网接收处理的入口函数,是硬件驱动与网络协议栈之间的桥梁,主要负责协调硬件数据读取与协议栈上传。它会先过滤无效帧,比如剔除 CRC 错误、长度错误以及目标 MAC 地址既不匹配本机也非广播地址的帧,然后调用硬件驱动层的low_level_input()
函数,从硬件接收缓冲区读取有效数据并将其封装为 lwIP 协议栈可处理的pbuf
结构体。
硬件驱动层low_level_input():low_level_input()
的作用是直接操作硬件,完成数据从硬件缓冲区到软件数据结构的转换。它会从硬件接收缓冲区读取包括以太网帧头、payload 等在内的原始数据,然后按照 lwIP 协议栈的要求,将这些原始数据封装为包含数据指针、长度等信息的pbuf
结构体,以便协议栈后续处理,生成的pbuf
结构体会传递给网络接口层的netif_input()
函数。
网络接口层netif_input():netif_input()
的主要功能是根据以太网帧类型,将接收的pbuf
分发到对应的网络层协议。它会解析以太网帧头部的类型字段(EtherType),以此识别帧承载的上层协议,比如 0x0800 表示封装的是 IP 协议数据,0x0806 表示封装的是 ARP 协议数据,之后调用对应协议的处理函数,如 IP 协议调用ip_input()
,ARP 协议调用etharp_input()
。
网络层ip_input():ip_input()
负责解析 IP 头部,处理 IP 层逻辑并将数据传递到传输层。它会解析 IP 头部的版本、首部长度、协议类型、源 / 目的 IP 地址等信息,若数据包是 IP 分片,会将分片重组为完整的数据包,然后根据 IP 头部的协议类型字段识别传输层协议,如 0x06 对应 TCP 协议,调用tcp_input()
处理,0x11 对应 UDP 协议,调用udp_input()
处理,最终将重组后的完整数据传递到对应的传输层处理函数。
传输层udp_input():udp_input()
的作用是解析 UDP 头部,定位目标应用程序并传递数据。它会解析 UDP 头部的源端口、目标端口、长度、校验和等信息,然后根据目标端口在系统维护的 UDP 控制块(udp_pcb
,记录端口与应用程序的绑定关系)中查找对应的条目,找到匹配的udp_pcb
后,将 UDP 数据段(payload)传递到应用层注册的回调函数。
应用层:应用层回调函数是数据处理的终点,由用户实现,主要负责处理接收的数据,包括解析 payload、执行相关的业务逻辑处理等,完成数据从硬件到应用层的整个处理流程。