對於IP層主要討論信息包的接收、分片數據包重裝、信息包的發送和轉發三個內容。IP數據報頭結構如下所示,其中,選項字段是可以沒有的,所以通常的IP數據報頭長度爲20個字節。
第一個字段是4bit的版本號,對於IPv4
,該值爲4
;對於IPv6
,該值爲6
。
接下來的4bit字段用於記錄首部長度,以字爲單位。所以對於不含任何選項字段的IP報頭,則該長度值爲5
,由於該字段最大值爲15
,所以其能描述的最大IP報頭長度爲15*4=60
字節。
再下來是一個8bit的服務類型字段,該字段主要用於描述該IP數據包急需的服務類型,如最小延時、最大吞吐量、最高可靠性、最小費用等。這個字段在LWIP中沒啥用處。
16位的總長度字段描述了整個IP數據報,包括IP數據報頭的總字節數。理論上說,IP數據包總長度最大可達65535字節,但在實際應用中,底層鏈路可不允許這麼大的數據包出現在鏈路上,因爲這會大大增加數據出錯的可能性,所以在鏈路層往往會對大的IP數據包進行分片,當然這些都是後話。
接下來的16位標識字段用於標識IP層發送出去的每一份IP數據報,每發送一份報文,則該值加1。然後的3位標誌和13位片偏移字段用於在IP數據包分片時使用,這裏先不討論。LWIP的較高版本才支持IP分片功能。
TTL字段描述該IP數據包最多能被轉發的次數,每經過一次轉發,該值會減1,當該值爲0時,一個ICMP報文會被返回至源主機。
8位協議字段用來描述該IP數據包是來自於上層的哪個協議,該值爲1
表示爲ICMP
協議,該值爲2
表示IGMP
協議,該值爲6
表示TCP
協議,該值爲17
表UDP
協議。
16位首部校驗和只針對IP首部做校驗,它並不關心其內部數據在傳輸過程中出錯與否,對於數據的校驗是上層協議負責的,如ICMP、IGMP、TCP、UDP
協議都會計算它們頭部以及整個數據區的長度。這裏再COPY一段這個校驗和是怎樣生成以及在接收端是如何實驗校驗的。
在發送端爲了計算一份數據報的IP檢驗和,首先把檢驗和字段置爲 0。然後,對首部中每個 16 bit進行二進制反碼求和(整個首部看成是由一串 16 bit的字組成),結果存在檢驗和字段中。當接收端收到一份I P數據報後,同樣對首部中每個16 bit進行二進制反碼的求和。由於接收方在計算過程中包含了發送方保存在首部中的檢驗和字段,因此,如果首部在傳輸過程中沒有發生任何差錯,那麼接收方計算的結果應該爲全 1。如果結果不是全1(即檢驗和錯誤),那麼IP就丟棄收到的數據報。但是不生成差錯報文,由上層去發現丟失的數據報並進行重傳。
接下來是兩個32位的IP地址。最後一個字段是任選字段,不同的協議會選擇性的使用該字段,這裏也不討論。
現在來看看LWIP中是怎麼樣來描述這個IP數據報頭的,使用的結構體叫ip_hdr
:
struct ip_hdr {
PACK_STRUCT_FIELD(u16_t _v_hl_tos); // 前三個字段:版本號、首部長度、服務類型
PACK_STRUCT_FIELD(u16_t _len); // 總長度
PACK_STRUCT_FIELD(u16_t _id); // 標識字段
PACK_STRUCT_FIELD(u16_t _offset); // 3位標誌和13位片偏移字段
#define IP_RF 0x8000 //
#define IP_DF 0x4000 // 不分組標識位掩碼
#define IP_MF 0x2000 // 後續有分組到來標識位掩碼
#define IP_OFFMASK 0x1fff // 獲取13位片偏移字段的掩碼
PACK_STRUCT_FIELD(u16_t _ttl_proto); // TTL字段和協議字段
PACK_STRUCT_FIELD(u16_t _chksum); // 首部校驗和字段
PACK_STRUCT_FIELD(struct ip_addr src); // 源IP地址
PACK_STRUCT_FIELD(struct ip_addr dest); // 目的IP地址
} PACK_STRUCT_STRUCT;
注意結構體聲明的時候定義了幾個宏定義:IP_RF、IP_DF、IP_MF、IP_OFFMASK
,它們是在求與分組相關兩個字段時要用到的掩碼,也可以在結構體的外面進行定義,無影響。
前面講過,從以太網底層進來的數據包經過ethernet_input
函數分發給IP模塊或者ARP模塊,分發給IP模塊是通過調用ip_input
函數完成的,當然在遞交前,ethernet_input
需要將數據包去掉以太網頭。現在來看看數據包傳遞給ip_input
後,該函數進行了哪些方面的工作。這裏我們先不涉及其內部關於DHCP協議的相關處理。
第一件事是檢查IP頭部的版本號,如果該值不爲4,則立即丟棄該數據包。更高版本的LWIP協議棧可以支持IPv6,但這裏我們只討論IPv4。接下來函數檢查IP數據報頭是否只保存於一個pbuf中,如果不是 ,也直接丟棄該IP包,這是因爲LWIP不允許IP數據包頭被分裝在不同的pbuf裏面。同時,函數檢查IP報頭中的總長度字段是否大於遞交上來的數據包總長度,如果是,則說明存在傳輸錯誤,直接丟棄數據包。
然後是對IP數據報頭做校驗,該工作是函數inet_chksum
完成的,如果校驗不通過則直接丟棄數據包。inet_chksum
函數在後續有需要時會詳細講解。
接着,需要在這裏對數據包進行截斷操作,按照IP包頭記錄的總長度字段截取數據包,因爲經過ethernet_input
傳遞上來的數據包只被去除了以太網數據包頭部,而對於可能存在的以太網填充字段和一定存在的以太網校驗字段(最後一字節)沒做處理,我們在這裏對它們進行截斷,得到完整無冗餘的IP數據包。
然後,函數檢測IP數據包中的目的IP地址是否與本機的相符,本機的IP地址是保存在netif
結構體變量中的,一個系統可能有着多個網卡設備,這就意味着它有多個netif
結構體變量分別用於描述這些網卡設備,也意味着本機有着多個IP地址,這些netif
結構體是被連接在netif_list
鏈表上的。ip_input
函數會遍歷netif_list
鏈表上的netif
結構以找到匹配的IP地址,並記錄該netif
結構體變量,也即記錄該網卡。從這點看來,在ARP部分內容中,對於某個接收到的ARP請求包,也應該按照這種方式進行遍歷後再給出ARP相應更好,而源代碼並沒有這樣做,當然,這只是個人意見。當遍歷完成後,如果依舊沒有得到與匹配的netif
結構體變量,這說明該數據包不是給本機的,此時需要對數據包進行轉發或者丟棄工作,這是通過宏定義IP_FORWARD
來完成的,這裏注意不要對廣播數據包進行轉發。
再接下來,根據目標IP地址判斷數據包是否爲廣播或多播IP數據包,LWIP不對這些類型的數據包進行相應。
再接下來的工作可以說是**ip_input
函數中最複雜最難理解的部分,這就是IP分片數據包的重裝**,ip_input
函數通過數據包的3
位標誌和13
位片偏移字段判斷髮給自己的該IP包是不是分片包,如果是,則需要將該分片包暫存,等到接收完所有分片包後,統一將整個數據包遞交給上層應用程序。如果是分片包,且不是最後一片,則函數到這裏就返回了。
終於,能到達這一步的數據包必然是未分片的或經過分片完整重裝後的數據包。此時,ip_input
函數根據IP數據包頭內部的協議字段判斷該數據包應該被遞交給哪個上層協議,並調用相應的函數遞交數據包。是UDP
協議,則調用udp_input
函數;是TCP
協議,則調用tcp_input
函數;是ICMP
協議,則調用icmp_input
函數;是IGMP
協議,則調用igmp_input
函數;如果都不是,則調用函數icmp_dest_unreach
返回一個協議不可達ICMP
數據包給源主機,同時刪除數據包。