UDP 协议栈
- 类型:无连接、面向报文(message-oriented)的传输层协议(IP 之上)。
- 主要特性:简单、开销小、低延迟、不保证交付、不保证顺序、不重传(需上层处理可靠性)。
- 头部字段(简要):源端口、目的端口、长度、校验和。
- 校验和:IPv4 下 UDP 校验和可选(0 表示不计算),但 IPv6 下强制要求;在 lwIP 源码中有相应的检查/生成分支(你贴的 udp.c 就包含这些判断)。
- 端口用途:端口用于在同一主机上区分不同应用/服务(0--65535)。规定分区:0--1023(well-known),1024--49151(registered),49152--65535(dynamic/ephemeral)。你贴的 lwIP 代码用默认动态端口范围 0xC000(49152)到 0xFFFF(65535)分配本地端口。
目录(快速跳转)
- 接收路径(链路、匹配、校验、交付)
- 发送路径(构造、添加头、校验、路由、下发)
- udp_pcb 与 udp_pcbs 链表(bind/connect/recv)
- 广播/多播与特别处理
- pbuf 与内存管理要点
- 常见问题与调试点(抓包 + 源码断点建议)
- 关键函数清单(便于查源码)
1. 接收路径(从以太网/IP 到应用)
序列:ethernet_input → ip4_input → udp_input → udp_input 内部匹配/校验 → 回调 recv
要点:
- tcpip_thread 上下文:udp_input 在 lwIP core 线程(tcpip_thread)中执行(LWIP_ASSERT_CORE_LOCKED())。这保证了 PCB 列表访问的并发安全。见 udp_input() 开头断言。
- 最小长度检查:p->len < UDP_HLEN 则丢弃(udp_input 起始)。
- 端口/地址提取:从 UDP 首部取得 src/dest 端口(lwip_ntohs),并用 ip_current_src_addr()/ip_current_dest_addr() 访问当前 IP 层的"全局"来源/目的地址。
- 本地匹配策略(关键函数:udp_input_local_match)
- 优先选择"完全匹配"(local_port == dest 且 remote_ip/remote_port 已绑定且相符)。
- 若无完全匹配,选择第一个"unconnected but matching local" PCB(uncon_pcb),对于 broadcast/multicast 有额外优先规则。
- 校验(CHECKSUM_CHECK_UDP):对匹配到的或目的为本机的包做 UDP 伪首部校验(ip_chksum_pseudo 或 ip_chksum_pseudo_partial)。校验失败会丢弃(并统计)。
- pbuf 操作:成功后调用 pbuf_remove_header(p, UDP_HLEN) 将 p->payload 指向 UDP 数据区(注意:函数可能链式 pbuf),然后传给 PCB 的回调:pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src)。回调负责释放 pbuf(文档注释)。
行为结果:
- 找到 pcb 且 pcb->recv != NULL → 将 p 交给回调(上层应用或 netconn/socket 层)。
- 找不到匹配 pcb → 非广播/组播 时发送 ICMP port unreachable(icmp_port_unreach),然后丢弃 pbuf。
- SO_REUSE / SO_REUSEADDR 等选项会影响匹配规则(参考 udp_input 中 SO_REUSE 分支)。
2. 发送路径(从应用到网卡)
序列(raw API):udp_send / udp_sendto → udp_sendto_if_src → 构造 UDP 头(pbuf_add_header)→ 校验/设置 udphdr → ip_output_if_src/ip_output_if → netif->linkoutput(low_level_output)
要点:
- 绑定检查:若 pcb 未绑定本地端口(pcb->local_port == 0),udp_send 系列会在发送前调用 udp_bind() 自动选择本地端口(udp_new_port)。
- pbuf 和头部:如果第一个 pbuf 无法容纳 UDP 首部(q->len < sizeof(struct udp_hdr)),会分配新的头部 pbuf(PBUF_IP, PBUF_RAM)并链入(pbuf_chain)。发送完成后若为单独头部会被释放(函数末尾处理)。
- 校验(CHECKSUM_GEN_UDP):计算 UDP 伪首部校验并写入 udphdr->chksum;若结果 0 则置为 0xffff(zero 表示"无校验")。
- 源地址选择:在udp_sendto_if_src 中会基于 pcb->local_ip 与 netif 决定 src_ip(IPv4/IPv6/多播有特殊逻辑)。
- 最终调用:ip_output_if_src(q, src_ip, dst_ip, ttl, tos, IP_PROTO_UDP, netif) 完成路由与 IP 层下发;IP/Netif 层会把 pbuf 发给 netif->linkoutput(如 ethernetif_output),由驱动启动 DMA 发帧。
性能/正确性提示:
- 为避免额外 pbuf 拷贝,优先让应用提供第一个 pbuf 足够放 UDP/IP 头。
- 发送时若 netif 为空或路由失败,udp_sendto 返回 ERR_RTE。
3. udp_pcb 与 udp_pcbs 链表(管理、bind、connect、recv)
数据结构:struct udp_pcb(定义在 udp.h)包含 IP_PCB(local_ip/remote_ip 等)、local_port、remote_port、flags、recv 回调和 next 指针。全局头:udp_pcbs(链表头)。
常用 API:
- udp_new() / udp_new_ip_type():分配 PCB(memp_malloc)。
- udp_bind(pcb, ipaddr, port):绑定本地 IP/端口并将 pcb 插入 udp_pcbs 列表(检查重复绑定)。
- udp_connect(pcb, ipaddr, port):设置远端地址/端口(并将 pcb 插入 udp_pcbs)。connect 会使 pcb 变为"已连接",之后可以用 udp_send() 省略目标地址参数。
- udp_recv(pcb, recv_fn, recv_arg):注册接收回调。回调原型:void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port); 回调负责释放 pbuf。
- udp_remove(pcb):删除并释放 PCB。
匹配行为总结(udp_input 内): - 遍历 udp_pcbs,优先完全匹配(local_port + remote_port + remote_ip);否则选择第一个匹配 local_port 的"unconnected" pcb(uncon_pcb)。对广播/多播和 SO_REUSE 有额外逻辑。
4. 广播 / 多播 / ICMP 处理
-
广播:IP 广播 + MAC 广播 二者共同作用,udp_input 会检测 broadcast = ip_addr_isbroadcast(...),并在匹配时考虑 SOF_BROADCAST 选项(IP_SOF_BROADCAST_RECV)。
-
多播:若 pcb 注册了 multicast loop/ifindex 等,会在发送时设置 PBUF_FLAG_MCASTLOOP 并选择合适 netif。接收时基于目的地址做匹配。
-
未找到 PCB:除广播/多播情况外,udp_input 会触发 icmp_port_unreach() 给发送端发送 ICMP Port Unreachable。
-
广播由两层共同实现:IP 层决定"逻辑上要广播"(目的 IP 如 255.255.255.255 或 子网广播地址),链路层把以太网目的 MAC 置为 ff:ff:ff:ff:ff:ff 以实现物理广播。lwIP 在 udp_input/udp_send 系统里处理这些逻辑。
-
发送广播:
- 使用 sockets API:必须允许广播(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, ...)),然后 sendto() 到广播 IP(或子网广播 IP)。
- 使用 lwIP raw/netconn:需要设置相应的 PCB 选项以允许广播(源码中有关 SOF_BROADCAST / IP_SOF_BROADCAST 的检查),然后调用 udp_sendto/udp_sendto_if。
- 发送时,udp_sendto_if_src 中会检测目标是否为广播并要求 PCB 具有广播权限(否则返回错误)。
-
接收广播:
- 将 UDP PCB 绑定到合适的本地端口/IP(INADDR_ANY 通常能接收广播),并注册回调(udp_recv)或使用 socket/netconn;udp_input 会识别 broadcast 并在匹配时考虑广播规则(包括 SO_REUSE / SO_REUSEADDR 等)。
- 网卡/驱动与交换机必须允许广播帧(有些硬件/过滤规则会丢弃广播)。
-
额外注意:
- 检查 lwIP 配置项(如 IP_SOF_BROADCAST_RECV / IP_SOF_BROADCAST)和是否开启 IPv4 支持。
- 广播受路由器限制:本地子网内有效,定向广播可能被路由器阻断。
- IPv6 无广播,仅用多播。
-
简短示例(sockets):
c
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
struct sockaddr_in dst;
dst.sin_family = AF_INET;
dst.sin_port = htons(12345);
dst.sin_addr.s_addr = inet_addr("192.168.0.255"); // 或 "255.255.255.255"
sendto(fd, buf, len, 0, (struct sockaddr*)&dst, sizeof(dst));
5. pbuf 内存管理(接收端与发送端须知)
- 接收端:udp_input 在把 p 交给 pcb->recv 后不再释放 p;回调必须负责 pbuf_free§ 或 pbuf_chain 的管理(文档注释)。因此:
- 如果上层复制数据(例如把 p 数据复制到用户 buffer),回调仍需 free。
- 若上层要延长 p 使用(异步处理),必须增加引用或把 p 换成 pbuf_clone(避免后续释放导致悬指针)。
- 发送端:udp_send 系列在需要时会为头部分配临时 pbuf(q)。发送成功/返回前会释放头部 pbuf(若有),而原始 pbuf 由调用者负责(udp_send 不 free 传入 p)。
6. 常见问题与调试点(抓包 + 源码断点)
- 抓包显示到达但应用未收到:断点 tcpip_thread 中调用 ethernet_input → udp_input,检查 pcb 是否匹配(端口/地址/netif),并确认 pcb->recv 已注册。
- pbuf 在回调后未释放导致内存泄漏:定位回调函数(udp_recv 注册点或 socket/netconn 层)是否未调用 pbuf_free 或未传递给 netconn。
- ICMP port unreachable:说明到达主机但没有可匹配的 pcb,检查绑定的端口与地址(udp_bind)与 SO_REUSE 设置。
- 校验失败丢弃:开启 UDP Debug 或在 udp_input 中查看 CHECKSUM_CHECK_UDP 路径,注意网卡是否做了校验卸载(硬件可能已校验,软件应跳过)。
- 发送失败 ERR_RTE:在 udp_sendto_if 中断点检查 ip_route 返回值与 netif 是否为 NULL。
建议断点位置:
- 接收链:tcpip_thread → ethernet_input → ip4_input → udp_input(在匹配环节设置断点)
- 发送链:调用点 udp_sendto_if_src → pbuf_add_header / ip_chksum_pseudo → ip_output_if_src → netif->output
- PCB 管理:udp_bind / udp_connect / udp_remove / udp_recv
7. 关键源码函数索引(快速跳转)
- 接收与分发:ethernet_input → ip4_input / ip6_input → udp_input → udp_input_local_match → pcb->recv 回调
- 发送:udp_send / udp_sendto / udp_sendto_if / udp_sendto_if_src → ip_output_if_src → netif->linkoutput
- PCB 管理:udp_new / udp_bind / udp_connect / udp_recv / udp_remove
- 校验与 pbuf:ip_chksum_pseudo / pbuf_remove_header / pbuf_add_header / pbuf_chain
8. 示例序列(Mermaid,接收与发送)
NIC_ISR / DMA ethernetif_input tcpip_input tcpip_thread ethernet_input ip4_input udp_input 应用回调 (udp_recv) xSemaphoreGiveFromISR 唤醒接收任务 low_level_input() 构造 pbuf netif->>input(p) (tcpip_input) 投递消息 ethernet_input(p) pbuf_remove_header() ip4_input(p) dispatch 到 udp_input(p) 匹配 pcb、校验、pbuf_remove_header(UDP_HLEN) pcb->>recv(..., p, src) 处理并 pbuf_free(p) NIC_ISR / DMA ethernetif_input tcpip_input tcpip_thread ethernet_input ip4_input udp_input 应用回调 (udp_recv)
9. 结论
- UDP 在 lwIP 中是基于 pcb 链表进行端口/地址匹配,收到数据即交给注册的 recv 回调,回调负责释放 pbuf。
- 发送端要注意 pbuf 布局(首 pbuf 是否能放 header)与源地址选择;udp_send 不会自动释放传入的 pbuf。
- 调试重点是"pcb 是否存在/匹配","pbuf 是否被回调释放","路由是否成功/NETIF 是否可用",以及"硬件是否做了校验卸载"。
10. 参考(源码文件)
- src/core/udp.c(udp_input、udp_sendto_if_src、udp_bind、udp_recv 等实现)
- include/lwip/udp.h(struct udp_pcb 定义与 API)
- src/core/ip.c / ip4.c(ip 层与路由)
- netif 驱动(ethernetif.c)和 tcpip.c(tcpip_thread / tcpip_input)