IP 協議提供了在各台主機之間傳送數據報的功能,但是各個主機並不是數據報中數據的最終目的地,數據的最終目的地應該是主機上的某個特定應用程序。那么 IP 層怎么樣將數據報遞交給各個應用程序呢?這就是傳輸層協議的功能了,TCP/IP 協議分層中,典型的傳輸層協議有 UDP和 TCP 兩種。UDP 為兩個應用程序提供了簡單的數據交互方式,有着很高的數據遞交效率,在局域網環境或在視頻播放領域有着廣泛的應用。另一方面,UDP 也是實現多種著名上層應用協議的基礎,例如 DNS、DHCP、IGMP、SNMP 等協議都使用 UDP 傳送協議數據。Anyway,本章中將涉及的知識點如下:
UDP 協議與端口號;
UDP 報文格式、校驗和、報文交付方式;
LwIP 中的 UDP 報文數據結構和 UDP 控制塊數據結構;
UDP 控制塊操作函數;
UDP 報文的發送處理、接收處理;
基於 UDP 的回顯測試程序。
一、背景知識
1、傳輸層協議
UDP 和TCP 都屬於傳輸層協議,前面講解的 IP 協議只能完成數據報在互聯網中各主機之間的遞交,IP 協議中,數據報的目的地是某一台主機,而不是主機上的某個應用程序。
要實現進程到進程間的通信,傳輸層協議需要完成幾個重要任務:
第一,為兩個通信的進程提供連接機制,即傳輸層將怎樣去識別兩個正在通信的進程,當主機的傳輸層從 IP 層得到一個數據報時,它將使用何種方式把數據遞交給最終的應用程序?在傳輸層中,這是通過端口號來完成的;
第二,傳輸層應該提供數據傳送服務,在數據發送端,傳輸層將用戶數據進行組裝、編號,將數據分割成可運輸的單元,然后依次遞交給 IP 層發送出去。接收端傳輸層等待屬於同一應用程序的所有數據單元到達,對它們進行差錯校驗,最后將整個數據交付給應用程序;
第三,為了提供更可靠的傳輸服務,傳輸層還應該提供流量控制機制,例如數據的確認、重傳等,以保證數據在兩個應用程序之間遞交的有效性。
2、UDP協議
UDP 稱為用戶數據報協議,是一種無連接的、不可靠的傳輸協議,它只在低級程度上實現了上述的傳輸層功能。UDP 只是簡單地完成數據從一個進程到另一個進程的交付,它沒有提供任何流量控制機制,收到的報文也沒有確認;在差錯控制上,UDP 只提供了一種簡單的差錯控制方法,即校驗和計算,當 UDP 收到的報文校驗和計算不成功時,它將丟棄掉這個報文。
UDP 協議的可靠性如此差,那為何還要使用它呢?
首先,這里的不可靠定義還是要根據具體使用環境來的,在現在的高可靠性、低時延的局域網環境下,使用 UDP 協議出現傳輸錯誤的可能性很小,但使用 UDP 卻可以帶來數據遞交效率和處理速度的提升,因為它省去了連接建立、數據確認、流量控制等一系列過程。
從代碼的實現角度講,UDP 協議的代碼量非常小,對於小型嵌入式設備來說,在局域網中使用 UDP 來實現通信還是很合適的。
除此之外,UDP 也經常在那些對輕微數據差錯不敏感的應用中被使用到,例如實時視頻傳輸、網絡電話等。
3、端口
每台主機都包含了一組稱為協議端口的抽象目的點,每個協議端口用一個正整數來標志,在TCP/IP 協議簇中,端口號范圍為 0~65535,進程可以綁定到某一個端口號上,UDP 報文需要在其內部指出該報文應該遞交的目的端口號,這樣,綁定到相關端口號的進程將最終得到數據報文。兩個進程要進行互相間的通信,它們都必須知道對方的 IP 地址和綁定的端口號。
UDP 協議的端口分配方法可以分為兩大類,第一種是一些中央授權機構已經明確規定功能的端口號,稱之為熟知端口號(wellknown port assignment),這些端口號與實現某些上層協議的功能密切相關;第二種端口分配方法稱為動態綁定(dynamic binding),這種類型的端口號稱為短暫端口號。
服務器進程必須綁定到一個熟知的端口號上,這個端口號是通信雙方事先都知道的,客戶端進程可以直接往該端口號上發送數據,這樣數據就能正確到達目的主機上的服務器程序了。UDP 中常見的熟知端口號及其作用如表 121 所示,提供相關功能的服務器進程必須綁定到對應的端口號上 。
另一方面,客戶端程序也必須使用一個端口號來標識自己,這個端口號可以在允許范圍內隨機的選取(短暫端口號),但最好不要使用熟知端口號,客戶端可以在報文中攜帶這個端口號,服務器進程通過這個端口號就能向客戶端進程返回數據 。
4、UDP報文的交付
用戶進程使用 UDP 來傳送數據時,UDP 協議會在數據前加上首部組成 UDP 報文,並交給 IP協議來發送,而 IP 層將報文封裝在 IP 數據報中並交給底層發送,在底層,IP 數據報會被封裝在物理數據幀中。因此,一份用戶數據在被發送時,經歷了三次封裝過程,如圖 121 所示。
在接收端,物理網絡先接收到數據幀,然后逐層地將數據遞交給上層協議,每一層都在向上層遞交前去除掉一個首部。在 UDP 層,它將從 IP 層得到 UDP 報文,UDP 協議會根據該報文首部的目的端口字段將報文遞交給用戶進程,綁定到這個目的端口的進程將得到報文中的數據。
5、UDP報文格式
如圖 122 展示了一份 UDP 報文的具體結構,UDP 首部很簡單,它由四 個 16 位字段組成,分別指出了該用戶數據報從哪個端口來、要到哪個端口去、總長度和校驗和。
在用戶數據報的發起端(通常作為客戶機),通常會將目的端口號填寫為服務器上某個熟知的端口,對源端口號字段的填寫則是可選的,如果客戶端期望服務器為自己返回數據,則必須填寫源端口號字段,服務器會在收到的報文中提取到這個源端口號,並在返回數據時使用到。
16 位的總長度字段定義了用戶數據報的總長度,包括首部長度和數據區長度,以字節為單位。 UDP數據區的數據最多只能有 65507 字節(65535-8-20),因為我們在講解 IP 數據報首部時,IP 首部中的總長度字段也為 16 位,UDP 要使用 IP 層來傳送數據報,所以其數據長度也必須滿足 IP 首部中的長度要求。
如果不使用校驗和,可以直接將該字段填入 0,之所以可以不使用校驗和,是因為在某些特殊場合,例如在高可靠性的局域網中使用 UDP 時,減少校驗和的計算能增加 UDP 的處理速度。在以太網的底層物理幀接收過程中,通常會對整個數據幀進行 CRC 校驗,因此,數據報出錯的可能性已被降到最低。
二、UDP數據結構
源文件 udp.h 和 udp.c 中包含了與 UDP 協議實現相關的所有數據結構和函數,這節來看看 UDP有哪些重要數據結構。
1、報文首部結構
源代碼用結構體 udp_hdr 定義了 UDP 報文首部中的各個字段,首部結構如圖 122 所示。
————udp.h—————————————————————— #define UDP_HLEN 8 //定義 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 ——————————————————————————————————
2、控制塊
系統為每一個連接分配一個 UDP 控制塊,並把它們組織在一個全局的鏈表上,當 UDP 層收到IP 層遞交的報文時,會去遍歷這個鏈表,找出與報文首部信息匹配的控制塊,並調用控制塊中注冊的函數最終完成報文的處理。定義 UDP 控制塊時,會用到了 IP 層中定義的一個宏,先看看這個宏的結構。
————ip.h———————————— //下面定義宏 IP_PCB,它是與 IP 層相關的字段 #define IP_PCB \ struct ip_addr local_ip; \ //本地 IP 地址 struct ip_addr remote_ip; \ //遠端 IP 地址 u16_t so_options; \ //socket 選項 u8_t tos; \ //服務類型 u8_t ttl //生存時間(TTL) //定義 IP 控制塊 struct ip_pcb { IP_PCB; //宏 IP_PCB 相關的字段 }; ————————————————————————————
在 ip.h 文件中定義了宏 IP_PCB,這個宏在本章的 UDP 控制塊以及下一章的 TCP 控制塊中都會被用到,它定義了在這些控制塊中都會使用到的與 IP 協議相關的字段,這也是為什么會把這個宏放在 ip.h 文件中的原因。
需要指出的是,雖然宏 IP_PCB 中定義了很多字段,但是被使用最多的只有前兩個字段,即本地 IP 地址(源 IP 地址)和遠端 IP 地址(目的 IP 地址)。最后,上面的代碼還定義了一個結構體 ip_pcb,這里稱它為 IP 控制塊,它由宏 IP_PCB 包含的各 個字段組成。
下面來看正題,即 UDP 控制塊,源代碼如下:
————udp.h———————————————— //定義兩個宏,用於控制塊的 flags 字段,標識控制塊的狀態信息 #define UDP_FLAGS_NOCHKSUM 0x01U //不進行校驗和的計算 #define UDP_FLAGS_CONNECTED 0x04U //控制塊已和遠端建立連接 //定義 UDP 控制塊結構體 struct udp_pcb { IP_PCB; //宏 IP_PCB 中的各個字段 struct udp_pcb *next; //用於將控制塊組織成鏈表的指針 u8_t flags; //控制塊狀態字段 u16_t local_port, remote_port; //保存本地端口號和遠端端口號,使用主機字節序 void (* recv)(void *arg, struct udp_pcb *pcb, struct pbuf *p, //處理數據時的回調函數 struct ip_addr *addr, u16_t port); void *recv_arg; //當調用回調函數時,將傳遞給函數的用戶定義數據信息 }; ——————————————————————————————————
UDP 控制塊包含了宏 IP_PCB中定義的各個字段,會被多次使用到的是其中的本地 IP 地址和遠端 IP 地址。
鏈表頭指針為 udp_pcbs,next 字段就是用來構成鏈表的。 UDP 協議實現的本質是對鏈表udp_pcbs 上各個 UDP 控制塊的操作。
第一個標志該控制塊是否進行校驗和的計算,當 flags 的無校驗位(位 0)為 1 時,表示在發送報文時不計算首部中的校驗和字段,這樣的話在報文發送時,校驗和字段直接置 0 就可以了。第二個標志該控制塊是否處於連接狀態(位 2),當某個控制塊處於連接狀態時,表示它內部已經完整地記錄了關於通信雙方的 IP 地址和端口號信息。
當 UDP 接收到一個報文時,會遍歷鏈表 udp_pcbs 上的所有控制塊,檢查其中的本地端口號與報文首部中的目的端口號是否匹配,並將報文遞交給匹配成功的控制塊處理。
用戶程序在初始化一個控制塊時,需要在該字段注冊自定義的報文處理函數,在內核接收到報文並匹配到某個控制塊后,通過函數指針 recv 來回調用戶自定義的處理函數,這樣就最終完成了報文向用戶程序的遞交 。
關於函數指針 recv,來看看它的具體定義:
void (* recv)(void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *addr, u16_t port);
參數 arg 表示將傳遞給函數的用戶自定義數據;
參數 pcb 指向接收到報文的 UDP控制塊結構;
參數 p 指向接收到的報文 pbuf;
參數 addr 表示發送該報文的源主機 IP 地址;
參數 port 表示發送該報文的源主機上的端口號,用戶應用程序應該合理地使用這些參數傳進來的值完成對報文中數據的處理。
最后一個字段 recv_arg 的作用就很明顯了,它可以用來指向一個用戶自定義的數據信息,在回調 recv 指向的函數時,recv_arg 會作為函數的第一參數。
最后需要重點說明的是,控制塊中的最后兩個字段具有很重要的作用,它們是用戶程序與協議棧內核進行通信的紐帶,在后面講解其他兩種類型的 API 函數可以看到,API 函數的實現機制都需基於這兩個字段
從圖中可以看出,由於前兩個控制塊中記錄了連接雙方的地址和端口號信息,所以它們處於連接狀態;而最后一個控制塊只記錄了本地 IP 地址和端口號,它處於非連接狀態。若此時 UDP 接收到一個目的端口號為 4321 的數據報,則內核函數會從鏈表起始處 udp_pcbs 開始查找整個鏈表,以找到具有本地端口為 4321 的控制塊,這里為第二個控制塊。當找到該控制塊后,控制塊的 recv 字段指向的函數 proc2 會被調用以處理報文數據,傳遞進 proc2 的參數包含了足夠的信息,用戶程序編寫的關鍵就在於如何處理這些信息。
上述這種應用程序編寫方法就叫做 raw/callback API 方法,在這種方式下的應用程序與協議棧內核處於用一個進程中,用戶程序通過回調的方式被協議棧調用,以取得協議棧中的數據,基於回調機制的應用程序會使得整個代碼的靈活性加大。另一方面,使用這種方式編程需要直接與內核交互,所以編程難度較大,對程序設計人員的要求較高。在后面讀者會看到,基於回調機制的程序編寫方式也存在着一些缺陷,使其不適合在大型應用程序的開發中使用。
三、控制塊操作函數
1、使用UDP編程
根據接收到的報文查找 UDP控制塊,然后調用注冊的用戶函數處理報文數據,如果用戶注冊的函數為空,則相應的報文會被直接刪除,這種情況下,沒有任何錯誤會被報告給源主機;當查找不到對應的控制塊時,UDP 會向源主機返回一個 ICMP 端口不可達差錯報告報文。
2、新建控制塊
任何想使用 UDP 服務的應用程序都必須擁有一個控制塊,並把控制塊綁定到相應的端口號上,在接收報文時,端口號將作為報文終點選擇的唯一依據。 在內存池中為 UDP 控制塊申請一個 MEMP_UDP_PCB 類型的內存池空間,並初始化相關字段。
————udp.c—————————————— //新建 UDP 控制塊 struct udp_pcb *udp_new(void) { struct udp_pcb *pcb; pcb = memp_malloc(MEMP_UDP_PCB); //為控制塊申請一個內存池空間 if (pcb != NULL) { //申請成功后,初始化各個字段 memset(pcb, 0, sizeof(struct udp_pcb)); //將所有字段全部清 0 pcb>ttl = UDP_TTL; //設置控制塊中的 TTL 字段 } return pcb; } ——————————————————————
3、綁定控制塊
當作為服務器程序時,必須手動為控制塊綁定一個熟知端口號,當作為客戶端程序時,手綁定端口號並不是必須的,此時,在與服務器通信前,UDP 會自動為控制塊綁定一個短暫端口號。端口號綁定的本質就是設置控制塊中的local_port 和 local_ip 字段,它還涉及對鏈表 udp_pcbs 的操作。
————udp.c—————————————————— //函數功能:為 UDP 控制塊綁定一個本地 IP 地址和端口號 //參數 pcb:指向要操作的控制塊指針 //參數 ipaddr:本地 IP 地址,若為 IP_ADDR_ANY(0),表示任意網絡接口結構的 IP 地址 //參數 port:本地端口號,若為 0,則函數將自動為控制塊分配一個有效的短暫端口號 err_t udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) { struct udp_pcb *ipcb; u8_t rebind; //全局變量,表示控制塊是否已在鏈表 udp_pcbs 中置 0 rebind = 0; //遍歷整個鏈表,查找控制塊 pcb,若控制塊已在鏈表中,則后續不再進行鏈表插入操作 for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb>next) { if (pcb == ipcb) { //找到控制塊 rebind = 1; //設置全局變量有效 } } ip_addr_set(&pcb>local_ip, ipaddr); //設置控制塊的本地 IP 地址字段 //如果 port 為 0,則要自動為控制塊尋找一個有效短暫端口 if (port == 0) { //自動尋找一個短暫端口號 #ifndef UDP_LOCAL_PORT_RANGE_START #define UDP_LOCAL_PORT_RANGE_START 4096 //定義起始短暫端口號 #define UDP_LOCAL_PORT_RANGE_END 0x7fff //定義結束短暫端口號 #endif //下面從第一個短暫端口開始,依次判斷該端口號是否已被其他控制塊占用,若未被 //占用,這就是我們要尋找的有效端口號 port = UDP_LOCAL_PORT_RANGE_START; ipcb = udp_pcbs; //從鏈表第一個控制塊開始 while ((ipcb != NULL) && (port != UDP_LOCAL_PORT_RANGE_END)) { if (ipcb>local_port == port) { //如果端口號被當前控制塊占用 port++; //檢查下一個端口號 ipcb = udp_pcbs; //ipcb 復位,指向鏈表首部 } else ipcb = ipcb>next; //端口未被當前控制塊占用,檢查下一個控制塊 }//while if (ipcb != NULL) { //查找結束后,若 ipcb 不為空,則說明未找到有效可用端口 return ERR_USE; //返回端口被占用錯誤 } } pcb>local_port = port; //到這里,有了有效的端口號,直接設置 local_port 字段 if (rebind == 0)
{ //如果控制塊沒有在鏈表中,則將它加入鏈表的首部 pcb>next = udp_pcbs; udp_pcbs = pcb; } return ERR_OK; //返回處理結果 } ——————————————————————————————
該函數本質是設置控制塊的 local_port 和 local_ip 字段,並把控制塊加入到鏈表 udp_pcbs 中,這里涉及一個重綁定的問題,即如果控制塊已經在鏈表匯中,說明已經對其進行過綁定工作,這里就只是重新修改 local_port 和 local_ip 字段,並不需要再進行插入鏈表操作。
4、連接控制塊
與綁定控制塊函數相對應,連接控制塊函數完成控制塊中 remote_ip 和 remote_port 的設置。只有綁定了本地 IP 地址和端口號,以及遠端 IP 地址和端口號的控制塊才會處於連接狀態。
————udp.c—————————————————— //函數功能:為 UDP 控制塊綁定一個遠端 IP 地址和端口號 //參數 pcb:指向要操作的控制塊 //參數 ipaddr:遠端 IP 地址 //參數 port:遠端端口號 err_t udp_connect(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) { struct udp_pcb *ipcb; if (pcb>local_port == 0) { //如果本地端口號未綁定,調用函數綁定本地端口 err_t err = udp_bind(pcb, &pcb>local_ip, pcb>local_port); if (err != ERR_OK) //如果綁定失敗,則返回錯誤 return err; } ip_addr_set(&pcb>remote_ip, ipaddr); //設置 remote_ip 字段 pcb>remote_port = port; //設置 remote_port 字段 pcb>flags |= UDP_FLAGS_CONNECTED; //控制塊狀態設置為連接狀態 for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb>next) {//遍歷鏈表,查找是否控制塊 if (pcb == ipcb) { //已經處在鏈表中 return ERR_OK; //若是,則返回成功 } } //若控制塊沒有在鏈表中,則將控制塊插入到鏈表首部 pcb>next = udp_pcbs; udp_pcbs = pcb; return ERR_OK; } ——————————————————————————————————
5、其他控制塊操作函數
還有幾個控制塊操作函數,在應用程序的編寫過程中也經常用到,它們的代碼和功能都很簡單,如下所示:
————udp.c—————————————————— //函數功能:清除 remote_ip 和 remote_port 字段,將控制塊置為非連接狀態 void udp_disconnect(struct udp_pcb *pcb) { ip_addr_set(&pcb>remote_ip, IP_ADDR_ANY); //清空 remote_ip pcb>remote_port = 0; //清空 remote_port 字段 pcb>flags &= ~UDP_FLAGS_CONNECTED; //置為非連接狀態 } //函數功能:為控制塊注冊回調函數 //參數 pcb:指向要操作的控制塊 //參數 recv:用戶自定義的數據報處理函數 //參數 recv_arg:用戶自定義數據 void udp_recv(struct udp_pcb *pcb, void (* recv)(void *arg, struct udp_pcb *upcb, struct pbuf *p,struct ip_addr *addr, u16_t port),void *recv_arg) { pcb>recv = recv; //填寫控制塊的 recv 字段 pcb>recv_arg = recv_arg; //填寫控制塊的 recv_arg 字段 } //函數功能:將一個控制塊結構從鏈表中刪除,並釋放其占用的內存空間 void udp_remove(struct udp_pcb *pcb) { struct udp_pcb *pcb2; if (udp_pcbs == pcb) { //如果控制塊在鏈表的首部 udp_pcbs = udp_pcbs>next; //從鏈表上刪除 } else //否則,依次查找鏈表 for (pcb2 = udp_pcbs; pcb2 != NULL; pcb2 = pcb2>next) { if (pcb2>next != NULL && pcb2>next == pcb) { //找到控制塊 pcb2>next = pcb>next; //從鏈表上刪除 } } memp_free(MEMP_UDP_PCB, pcb); //釋放內存池空間 } ————————————————————————————————
四、報文處理函數
1、報文的發送
數據第一次封裝(UDP):err_t udp_send(struct udp_pcb *pcb, struct pbuf *p)
udp_pcb控制塊 + pbuf數據塊
數據第二次封裝(IP):err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *dst_ip, u16_t dst_port)
ip控制塊 + udp_pcb控制塊 + pbuf數據塊
數據第三次封裝(netif):err_t udp_sendto_if(struct udp_pcb *pcb, struct pbuf *p,struct ip_addr *dst_ip, u16_t dst_port, struct netif *netif)
netif控制塊 + ip控制塊 + udp_pcb控制塊 + pbuf數據塊
2、報文的接收
struct udp_pcb *udp_pcbs; //全局變量,指向 UDP 控制塊鏈表 //函數功能:UDP 報文處理函數 //參數 pbuf:IP 層接收到的包含 UDP 報文的數據報 pbuf,payload 指針指向 IP 首部 //參數 inp:接收到 IP 數據報的網絡接口結構 void udp_input(struct pbuf *p, struct netif *inp) { struct udp_hdr *udphdr; //UDP 報文首部結構 struct udp_pcb *pcb, *prev; //UDP 控制塊指針,用於查找過程 struct udp_pcb *uncon_pcb; //指向第一個匹配的處於非連接狀態的控制塊 struct ip_hdr *iphdr; //IP 數據報首部結構 u16_t src, dest; //保存報文中的源端口與目的端口 u8_t local_match; //志控制塊是否匹配 u8_t broadcast; //記錄該 IP 數據報是否為廣播數據報 iphdr = p->payload; //指向 IP 數據報首部 //進行長度校驗:整個數據報的長度不小於 IP 首部+UDP 首部的大小 //移動數據報 payload 指針,使其指向 UDP 首部 if (p->tot_len < (IPH_HL(iphdr) * 4 + UDP_HLEN) || pbuf_header(p, -(s16_t)(IPH_HL(iphdr) * 4))) { pbuf_free(p); //如果檢驗不通過或操作不成功,則釋放掉 pbuf goto end; //跳到 end 處,執行返回操作 } udphdr = (struct udp_hdr *)p->payload; //指向 UDP 報文首部 broadcast = ip_addr_isbroadcast(&(iphdr->dest), inp); //判斷 IP 數據報是否為廣播 src = ntohs(udphdr->src); //取得 UDP 首部中的源端口號 dest = ntohs(udphdr->dest); //取得 UDP 首部中的目的端口號 //下面開始查找匹配的 UDP 控制塊,第一查找目標為與目的端口號和目的 IP 地址匹配 //且處於連接狀態的控制塊,找不到,則查找與目的端口號和目的 IP 地址匹配的第一個 //處於非連接狀態的控制塊 { prev = NULL; //指針清空 local_match = 0; //當前控制塊的匹配狀況 uncon_pcb = NULL; //第一個匹配的非連接狀態控制塊 for (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next) { //遍歷整個控制塊鏈表 local_match = 0; //當前控制塊不匹配 //先判斷控制塊中記錄的本地端口號、IP 地址與數據報中記錄的 //目的端口號、目的 IP 地址是否匹配 if ((pcb->local_port == dest) && //若端口號和 IP 地址都匹配 ( (!broadcast && ip_addr_isany(&pcb->local_ip)) || ip_addr_cmp(&(pcb->local_ip), &(iphdr->dest)) || (broadcast)) ) { local_match = 1; //當前控制塊匹配 if ((uncon_pcb == NULL) && //若當前控制塊為未連接態且 uncon_pcb 為空 ((pcb->flags & UDP_FLAGS_CONNECTED) == 0)) { uncon_pcb = pcb; //記錄下第一個匹配的非連接態控制塊 } } //前階段匹配成功,則繼續匹配控制塊中記錄的源端口號、源 IP 地址與數據報 //中記錄的源端口號、源 IP 地址是否匹配,以找到一個處於連接狀態的控制塊 if ((local_match != 0) && //前階段匹配成功 (pcb->remote_port == src) && //且源端口號匹配 (ip_addr_isany(&pcb->remote_ip) || //且源 IP 地址匹配 ip_addr_cmp(&(pcb->remote_ip), &(iphdr->src)))) { //到這里我們得到一個完全匹配的控制塊,若控制塊不在鏈表首部,則需 //要把該控制塊移到鏈表的首部,這樣能提高下次報文處理時的查找效率 if (prev != NULL) { //當前控制塊不為鏈表的首部 prev->next = pcb->next; pcb->next = udp_pcbs; udp_pcbs = pcb; } break; //跳出 for 循環,結束查找過程 } prev = pcb; //進行下一個控制塊的比較 }//for if (pcb == NULL) { //遍歷完鏈表上的所有控制塊,沒有找到完全匹配的連接態控制塊 pcb = uncon_pcb; //則將第一個匹配的非連接狀態控制塊作為匹配結果 } } //到這里,如果找到了匹配的控制塊,或者找不到控制塊,但是數據報確實是給本地的 //對於前者調用用戶注冊的回調函數處理數據,對於后者,為源主機返回一個端口不可 //達差錯報文,Anyway,先得進行校驗和的驗證 if (pcb != NULL || ip_addr_cmp(&inp->ip_addr, &iphdr->dest)) { if (udphdr->chksum != 0) { //數據報中已經填寫了校驗和字段,則必須驗證 if (inet_chksum_pseudo(p, (struct ip_addr *)&(iphdr->src), (struct ip_addr *)&(iphdr->dest), IP_PROTO_UDP, p->tot_len) != 0) { pbuf_free(p); //校驗和失敗,刪除數據報,並返回 goto end; } } //校驗和通過,則將報文中的數據遞交給用戶程序處理,先調整 payload 指針 if(pbuf_header(p, -UDP_HLEN)) { //指向報文中的數據區 pbuf_free(p); //調整失敗,則刪除整個數據報 goto end; } //如果有匹配的控制塊,則調用控制塊中注冊的用戶函數處理數據 if (pcb != NULL) { if (pcb->recv != NULL) { //如果注冊了用戶處理程序 //調用用戶函數,用戶函數要負責報文 pbuf 的釋放 pcb->recv(pcb->recv_arg, pcb, p, &iphdr->src, src); } else { //若沒有注冊用戶處理函數 pbuf_free(p); //釋放數據報 pbuf 並返回 goto end; } } else { //沒有匹配的控制塊,說明找不到匹配的端口號,返回端口不可達報文 if (!broadcast &&!ip_addr_ismulticast(&iphdr->dest)) { //不是廣播和多播包 pbuf_header(p, (IPH_HL(iphdr) * 4) + UDP_HLEN); //payload 指向 IP 首部 icmp_dest_unreach(p, ICMP_DUR_PORT); //發送端口不可達報文 } pbuf_free(p); //釋放數據包 } } else { //不是給本地的數據報 pbuf_free(p); //直接刪除數據報 } end: }