1. UDP說明
1.1 協議簡介
UDP(User Datagram Protocol):用戶資料報協議,是一種簡單、無連接、不可靠的傳輸協議,
無需建立連接、沒有提供任何流量控制、擁塞控制機制,收到的報文也沒有確認,因此UDP的傳輸速度快,但不能保證資料到達目的地,
與我們熟知的TCP協議一樣,都屬于OSI模型中的傳輸層協議,
1.2 UDP特點
-
無連接性
UDP可以提供無連接的資料傳輸服務,無需在通訊前建立連接,也無需在通訊結束后斷開連接,節省了維護連接的開銷, -
不可靠性
受自身協議的限制,UDP的傳輸是一種不靠傳輸方式,無法保證資料一定能完整有效的傳輸到目標, -
以報文為邊界
因為沒有TCP協議的資料編號和接收確認機制,UDP對于應用層交付的資料直接進行封裝傳輸,不會對報文進行拆分合并,在多包資料傳輸時可能出現亂序的現象, -
無流量和擁塞控制功能
UDP協議沒有流量控制和擁塞控制的機制,因此更適用于對資料連續性比完整性要求更高、對輕微的資料差錯不敏感的場景,如語音、視頻通話等, -
支持廣播、組播
不同于TCP協議只能實作一對一的單播通訊,UDP協議支持單播、廣播、組播通訊,實作一對一、一對多、多對多的資料傳輸,因此所有以廣播、組播方式通信的協議都是在UDP協議上實作的,如我們常見的DHCP、SNMP協議,
1.3 報文格式

本篇文章重點是UDP在LwIP中的實作,報文格式就不再展開介紹了,但還是可以直觀地看出UDP首部只有8位元組的長度(偽首部只參與校驗和的計算,不實際發送),貫徹了UDP的簡潔易用的特點,
2. UDP在LWIP上的實作
2.1. 資料結構
2.1.1 UDP控制塊
struct udp_pcb {
IP_PCB; //通用IP控制塊
struct udp_pcb *next; //下一節點的指標,用于構成控制塊鏈表
u8_t flags; //控制塊狀態
u16_t local_port, remote_port; //本地埠號、遠程埠號
udp_recv_fn recv; //處理網路接收資料的回呼
void *recv_arg; //用戶自定義引數,接識訓呼入參
};
同時,lwip在udp.c中創建了全域的udp控制塊指標,作為管理所有UDP控制塊的鏈表頭,
struct udp_pcb *udp_pcbs;
2.1.2 UDP首部
PACK_STRUCT_BEGIN
struct udp_hdr {
PACK_STRUCT_FIELD(u16_t src); //源埠
PACK_STRUCT_FIELD(u16_t dest); //目的埠
PACK_STRUCT_FIELD(u16_t len); //此次發送的資料報的長度
PACK_STRUCT_FIELD(u16_t chksum);//校驗和
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
報文格式中提到了UDP偽首部,但資料結構中沒有出現,那計算偽首部的功能是在哪里實作的呢?
找到計算UDP首部中的校驗和計算函式:
/**
* 計算首部校驗和
* @param p 待計算資料的pbuf指標
* @param proto 協議型別
* @param proto_len ip資料部分的長度
* @param src 源ip地址 (這里的IP是網路位元組序)
* @param dst 目標ip地址
* @return 創建的UDP控制塊結構體指標,創建失敗回傳NULL
*/
u16_t ip_chksum_pseudo(struct pbuf *p, u8_t proto, u16_t proto_len,const ip_addr_t *src, const ip_addr_t *dest)
例如在udp_sendto_if_src()中,資料發送之前需要計算出首部校驗和,可以看到源IP、目的IP等引數是現算現傳的,沒有再使用額外的資料結構來維護偽首部,
if (IP_IS_V6(dst_ip) || (pcb->flags & UDP_FLAGS_NOCHKSUM) == 0) {
u16_t udpchksum = ip_chksum_pseudo(q, IP_PROTO_UDP, q->tot_len,src_ip, dst_ip);
/*0表示“無校驗和,因此計算為0時需要改為0xffff*/
if (udpchksum == 0x0000) {
udpchksum = 0xffff;
}
udphdr->chksum = udpchksum;
}
2.2 介面函式
2.2.1. 創建/洗掉UDP控制塊
/**
* 創建UDP控制塊
* @return 創建的UDP控制塊結構體指標,創建失敗回傳NULL
*/
struct udp_pcb* udp_new(void);
udp_new()為創建的UDP控制塊申請記憶體空間、初始化控制塊,回傳創建的控制塊指標供后續操作,
/**
* 創建UDP控制塊
* @param type 控制塊的IP型別
* @return 創建的UDP控制塊結構體指標
*/
struct udp_pcb * udp_new_ip_type(u8_t type);
與udp_new()相似,都是創建UDP控制塊,區別是udp_new_ip_type()可以指定創建的UDP控制塊為IPV4 / IPV6 / IPV4+IPV6型別,而udp_new()默認創建IPV4的UDP控制塊,
以上兩個函式都很簡單,就不把函式體展開討論了,這里需要注意的是兩個創建函式都只創建了控制塊的記憶體空間,進行了簡單的初始化,并未將控制塊掛載到udp_pcbs鏈表中.
/**
* 洗掉UDP控制塊
* @param pcb UDP控制塊指標
*/
void udp_remove(struct udp_pcb *pcb);
{
struct udp_pcb *pcb2;
LWIP_ASSERT_CORE_LOCKED();
LWIP_ERROR("udp_remove: invalid pcb", pcb != NULL, return);
mib2_udp_unbind(pcb);
/* 判斷待洗掉的控制塊在鏈表開頭 */
if (udp_pcbs == pcb) {
/* 從將第二個控制塊作為鏈表頭 */
udp_pcbs = udp_pcbs->next;
} else {
/* 遍歷udp 控制塊鏈表 */
for (pcb2 = udp_pcbs; pcb2 != NULL; pcb2 = pcb2->next) {
/* 在鏈表中找到了該控制塊 */
if (pcb2->next != NULL && pcb2->next == pcb) {
/* 將該控制塊在鏈表中洗掉 */
pcb2->next = pcb->next;
break;
}
}
}
/* 釋放該控制塊的記憶體空間 */
memp_free(MEMP_UDP_PCB, pcb);
}
洗掉UDP控制塊,并將該控制塊從UDP控制塊鏈表中洗掉,最后釋放控制塊的記憶體空間,
通過udp_remove()以及后面的udp_connect()可以看到lwip對udp控制塊鏈表的管理方式:單向鏈表,每次添加新節點插到鏈表開頭,尾節點的next為NULL,簡單方便,但個人認為控制塊鏈表頭作為全域變數存放,使用時也沒有加鎖或者關中斷保護,在一些搶占式的作業系統中,是存在臨界區問題的,應用開發時應避免頻繁的對控制塊鏈表有操作,
2.2.2. 系結
/**
* 將UDP控制塊系結到一個本地IP和埠號上
* @param pcb UDP控制塊指標
* @param ipaddr 要系結的本地IP
* @param port 要系結的本地埠號,輸入0時會系結一個隨機埠
* @return 錯誤碼
*/
err_t udp_bind(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port);
udp_bind()除了將控制塊與指定的IP和埠號系結,還會檢查UDP控制塊是否掛載到了上文提到的全域UDP控制塊鏈表中、待系結的IP-埠號是否與鏈表中的其他控制塊重復,未掛載、未重復的話會執行掛載:
{
/* code... */
rebind = 0;
/* 遍歷udp控制塊鏈表 */
for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) {
/* 如果當前控制塊已在控制塊鏈表中 */
if (pcb == ipcb) {
/* 已掛載標志位置位 */
rebind = 1;
break;
}
}
/* code... */
/* 未掛載? */
if (rebind == 0) {
/* 將當前控制塊插入到鏈表頭 */
pcb->next = udp_pcbs;
udp_pcbs = pcb;
}
/* code... */
}
系結本地埠不是UDP通訊的必要步驟,因為如果沒有系結本地埠,呼叫sendto()時會分配一個隨機埠,
該介面一般是設備作UDPS時使用,在此場景下,存在UDPC先向UDPS發送資料的情況,因此需要預先知道UDPS的埠號,即UDPS需要系結某個埠而不能是隨機埠,
2.2.3. 連接/斷連
/**
* 將UDP與指定IP、埠“建立連接”
* @param pcb UDP控制塊指標
* @param ipaddr 要連接的目的IP
* @param port 要連接的目的埠
* @return 錯誤碼
*/
err_t udp_connect(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port);
- UDP是無連接的,因此udp_connect()并不會真的像TCP的connect()一樣去執行建立連接的網路互動,而只是在內部把目標IP和埠號與UDP控制塊系結,
ip和埠號系結成功后,函式內部會將該PCB的flag置位為已連接:
pcb->flags |= UDP_FLAGS_CONNECTED;
- udp_connect()會檢查控制塊是否系結了本地ip埠,如果未系結,會執行一次udp_bind(),系結到隨機埠,
- udp_connect()還會檢查該控制塊是否掛載到了udp控制塊鏈表中,未掛載的話執行掛載,
- udp_connect()同樣也不是UDP通訊的必要步驟,系結的優點在于系結后可以直接呼叫udp_send(),直接向系結的目標IP和埠發送資料,無需像呼叫udp_sendto()介面一樣每次指定目標IP和埠,同時提高了執行效率,recv()時防止受到其他IP資料,
- 該介面一般是設備作UDPC時使用,系結了目標IP后直接呼叫udp_send()發送,UDPS這類需要頻繁向不同目標IP、埠發送資料的應用顯然不適合使用該介面,
/**
* “斷開”UDP控制塊已經建立的連接
* @param pcb UDP控制塊指標
*/
void udp_disconnect(struct udp_pcb *pcb);
與udp_connect()同理,udp_disconnect()也不會真的執行斷開連接的互動,只是將控制塊中系結的遠程IP、埠號清零,并將flag的連接標志復位,也沒有將控制塊從鏈表中洗掉的操作,
pcb->remote_port = 0;
pcb->netif_idx = NETIF_NO_INDEX;
udp_clear_flags(pcb, UDP_FLAGS_CONNECTED);
2.2.4. 發送
err_t udp_send(struct udp_pcb *pcb, struct pbuf *p);
err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *dst_ip, u16_t dst_port);
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);
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);
-
四個函式是一層一層呼叫的,udp_send() -> udp_sendto() -> udp_sendto_if() -> udp_sendto_if_src(),
從udp_send()函式開始,只需要傳入UDP控制塊和pbuf指標,在每層的呼叫程序中根據控制塊中的資訊將目的ip、埠號、netif、源IP資訊逐步補全,最后通過ip_output_if_src()函式將資料傳輸到IP層繼續處理, -
實際開發中常用的兩個介面是send()和sendto(),如上文介紹,執行過connect()的UDP可以直接呼叫send()發送到固定IP埠,代碼設計上更加簡潔高效,而呼叫sendto()可以每次向不同目標IP埠發送,使用上更加靈活,
2.2.5. 接收
/**
* 為控制塊注冊接識訓呼
* @param pcb UDP控制塊指標
* @param recv 處理網路資料的接識訓呼
* @param recv_arg 觸發時傳入回呼的用戶自定義引數
*/
void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv,void *recv_arg);
udp層提供的方法是通過注冊接識訓呼的方式實作接收網路資料,
回呼型別:
/**
* udp接識訓呼
* @param arg 回呼注冊時設定的用戶自定義引數
* @param pcb UDP控制塊
* @param p pbuf指標(payload在這里)
* @param addr 資料來源IP
* @param port 資料來源埠號
*/
typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
注冊的回呼在udp_input()中被執行,而udp_input()由IP層的ip4_input()/ip6_input()觸發,網路端收到資料后,IP層會判斷資料協議是否為UDP協議,若是則將資料、發送方的資訊、用戶自定義資料傳入udp_input(),最終到達用戶設定的回呼中供使用,
3. 參考文獻:
- LwIP-2.1.0原始碼
- 《深入理解計算機網路》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/295736.html
標籤:其他
下一篇:STM32串口輸出字串
