主要内容参照https://doc.embedfire.com/net/lwip/zh/latest/doc/chapter14/chapter14.html#id6,整理出来自用。
1. UDP 报文首部结构体(udp_hdr)
为清晰定义 UDP 报文首部的各个字段,LwIP 设计了udp_hdr
结构体,其包含 4 个核心字段,具体结构通过代码定义,各字段功能如下:
src
与dest
:均为 16 位无符号整数(u16_t),分别表示 UDP 通信的源端口号和目的端口号,用于标识通信双方的应用进程。len
:16 位无符号整数,代表 UDP 报文的总长度(包括首部和数据部分)。chksum
:16 位无符号整数,用于 UDP 报文的校验和计算,保障数据传输的完整性(若值为 0 则表示不进行校验)。
结构体定义中使用PACK_STRUCT
相关宏,是为了确保结构体在内存中紧凑存储,避免因编译器对齐规则导致字段偏移,保证数据解析的准确性。
2. UDP 控制块(udp_pcb)
与 TCP 类似,LwIP 通过 "UDP 控制块" 管理 UDP 通信的所有关键信息,每个基于 UDP 的应用线程都会对应一个控制块,并与特定端口绑定,以便系统识别和处理该应用的 UDP 数据。
(1)控制块的核心组成
UDP 控制块结构体(udp_pcb
)的内容可分为两部分:
#define IP_PCB \
/* 本地ip地址与远端IP地址 */ \
ip_addr_t local_ip; \
ip_addr_t remote_ip; \
/* 网卡id */ \
u8_t netif_idx; \
/* Socket 选项 */ \
u8_t so_options; \
/* 服务类型 */ \
u8_t tos; \
/* 生存时间 */ \
u8_t ttl \
IP_PCB_NETIFHINT
/** UDP控制块 */
struct udp_pcb
{
IP_PCB;
//指向下一个控制块
struct udp_pcb *next;
//控制块状态
u8_t flags;
/** 本地端口号与远端端口号 */
u16_t local_port, remote_port;
/** 接收回调函数 */
udp_recv_fn recv;
/** 回调函数参数 */
void *recv_arg;
};
- 复用 IP 层信息 :通过引入
IP_PCB
宏,直接包含 IP 层通信所需的基础信息,如本地 IP 地址、远端(目标)IP 地址、网卡 ID(netif_idx)、Socket 选项(so_options)、服务类型(tos)和生存时间(ttl),避免信息重复定义,简化 IP 层与 UDP 层的交互。 - UDP 层专属信息 :包括控制块链表指针(
next
,用于连接多个控制块)、控制块状态标识(flags
)、本地端口号与远端端口号(local_port
、remote_port
,核心标识字段,用于匹配应用线程),以及接收数据的回调函数(recv
)和回调参数(recv_arg
,用于数据递交给上层应用)。
(2)控制块的管理方式
LwIP 会将所有 UDP 控制块通过next
指针串联成一个链表,链表的头节点由全局变量udp_pcbs
记录。这种链表结构便于系统在处理 UDP 数据时,通过遍历链表快速查找对应的控制块,提高数据处理效率。
(3)回调函数的注册
回调函数(udp_recv_fn
)是 UDP 层向应用层递交数据的关键接口,其函数原型(见原文代码清单 14_3)规定了参数格式:回调参数(arg
)、对应的 UDP 控制块(pcb
)、存储数据的缓冲区(pbuf
)、数据来源的 IP 地址(addr
)和端口号(port
)。
回调函数的注册通过udp_recv
函数实现:该函数将用户定义(或系统默认)的回调函数及其参数,分别赋值给控制块的recv
和recv_arg
字段。实际开发中,若使用 NETCONN API 或 Socket API,LwIP 内核会自动注册recv_udp
作为回调函数,无需用户手动实现;若使用 RAW API,则需用户自行定义并注册回调函数。
二、UDP 报文发送流程
UDP 作为传输层协议,需接收上层应用数据并添加首部后,交付给 IP 层发送,核心逻辑由udp_sendto_if_src
函数实现,整体流程简洁,具体步骤如下:
- 端口绑定检查 :若当前 UDP 控制块未绑定本地端口(
local_port
为 0),则先调用udp_bind
函数完成端口绑定,确保数据能被正确识别和处理。 - 数据长度与内存检查 :
- 校验数据总长度:判断添加 UDP 首部(长度为 UDP_HLEN)后,总长度是否超过 16 位整数的最大值(避免溢出),若超过则返回内存错误(ERR_MEM)。
- 检查缓冲区空间:尝试在当前数据缓冲区(
pbuf
)头部预留 UDP 首部空间,若空间不足,则新分配一个仅存储首部的pbuf
,并与原数据缓冲区链接成链表(pbuf_chain
)。
- 填充 UDP 首部 :将控制块中的本地端口号、目标端口号,以及缓冲区总长度(转换为网络字节序,通过
lwip_htons
函数),分别填入 UDP 首部的src
、dest
和len
字段;校验和字段(chksum
)默认设为 0(表示不校验,可根据需求调整)。 - 交付 IP 层发送 :调用
ip_output_if_src
函数,将封装好 UDP 首部的数据交付给 IP 层,由 IP 层负责通过指定网卡(netif
)发送到目标地址;发送完成后,根据缓冲区是否为新分配,决定是否释放内存(pbuf_free
),并更新相关统计指标(如udp.xmit
、mib2.udpoutdatagrams
)。
相较于 TCP,UDP 发送流程无需建立连接、重传确认等复杂逻辑,仅需完成首部封装和层间交付,处理效率更高。
三、UDP 报文接收流程
当 IP 层接收到 UDP 报文后,会调用udp_input
函数将数据递交给 UDP 层处理,核心是通过匹配控制块找到对应应用,并完成数据递交,具体步骤如下:
-
合法性初步校验:
- 检查报文长度:若当前缓冲区长度小于 UDP 首部长度(UDP_HLEN),则判定为无效报文,更新错误统计(如
udp.lenerr
)并释放缓冲区,直接结束处理。 - 解析基础信息:提取 UDP 首部(转换为
udp_hdr
类型),判断报文是否为广播包(ip_addr_isbroadcast
),并将源端口号、目的端口号从网络字节序转换为主机字节序(lwip_ntohs
)。
- 检查报文长度:若当前缓冲区长度小于 UDP 首部长度(UDP_HLEN),则判定为无效报文,更新错误统计(如
-
遍历控制块链表匹配应用:
- 遍历
udp_pcbs
链表,对比控制块的 "本地端口号" 与报文的 "目的端口号",同时通过udp_input_local_match
函数校验 IP 地址匹配性(本地 IP 与报文目的 IP),筛选出候选控制块。 - 进一步筛选 "完全匹配" 的控制块:在候选控制块中,对比控制块的 "远端端口号" 与报文的 "源端口号"、控制块的 "远端 IP" 与报文的 "源 IP",若均匹配,则确定为目标控制块;若存在多个候选,会将完全匹配的控制块移至链表头部,优化后续查找效率。
- 无完全匹配时的处理:若未找到完全匹配的控制块,会选取第一个未绑定远端信息(
UDP_FLAGS_CONNECTED
未置位)的候选控制块作为替代;若仍无候选,则判定为 "无对应应用"。
- 遍历
-
数据递交或差错反馈:
- 数据递交(找到对应控制块) :先从缓冲区中移除 UDP 首部(
pbuf_remove_header
),提取纯数据部分;若控制块已注册回调函数(recv
不为空),则调用该函数,将数据、源 IP、源端口等信息递交给上层应用(回调函数需负责后续缓冲区释放);若未注册回调函数,则直接释放缓冲区。 - 差错反馈(无对应控制块) :若报文非广播包或多播包,会构造 "端口不可达" 的 ICMP 差错报文(通过
icmp_port_unreach
函数),反馈给报文源主机,同时释放缓冲区并更新统计指标(如udp.proterr
、mib2.udpnoports
)。
- 数据递交(找到对应控制块) :先从缓冲区中移除 UDP 首部(
-
资源清理与统计更新 :处理结束后,释放相关资源(如缓冲区),停止性能计时(
PERF_STOP
),并更新 UDP 接收相关的统计数据(如udp.recv
、mib2.udpindatagrams
)。
