主要內容參照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
)。