前面一節重點說了ARP緩存表以及如何對其進行相關操作,關於ARP,一共想說三個函數,前面已經講過了兩個。
最後要講的一個函數是update_arp_entry
,該函數用於更新ARP緩存表中的表項或者在緩存表中插入一個新的表項。該函數會在收到一個IP數據包或ARP數據包後被調用。該函數原型如下,
static err_t
update_arp_entry(struct netif *netif, struct ip_addr *ipaddr, struct eth_addr *ethaddr, u8_t flags)
其中重要的兩個參數ipaddr
和ethaddr
分別對應的ip
地址和mac
地址,函數利用這兩個地址去更新或插入ARP表項。由於這個函數代碼量較小,這裏就列出源碼來講解,注意這個源碼是經過處理的,已經去掉了編譯選項、源碼註釋、調試輸出信息等非重點部分。
static err_t
update_arp_entry(struct netif *netif, struct ip_addr *ipaddr, struct eth_addr *ethaddr, u8_t flags)
{
s8_t i; // 兩個變量,不解釋
u8_t k;
i = find_entry(ipaddr, flags); // 查找或新建一個ARP表項,返回其索引值
if (i < 0) return (err_t)i; // 如果爲不合法的索引值,則更新緩存表失敗
arp_table[i].state = ETHARP_STATE_STABLE; // 否則將對應表項狀態改爲stable
arp_table[i].netif = netif; // 記錄下網絡接口
k = ETHARP_HWADDR_LEN; // 這一段是更新緩存表項中的MAC地址
while (k > 0) {
k--;
arp_table[i].ethaddr.addr[k] = ethaddr->addr[k];
}
arp_table[i].ctime = 0; // 生存時間值置0
#if ARP_QUEUEING //該ARP表項上有未發送的隊列,則把這些隊列發送出去
while (arp_table[i].q != NULL) {
// 只要緩衝鏈表中海有數據則循環
struct pbuf *p;
struct etharp_q_entry *q = arp_table[i].q; // 記錄下緩衝鏈表表頭
arp_table[i].q = q->next; // 緩衝鏈表表頭指向下一個節點
p = q->p; // 取得記錄下的緩衝鏈表表頭指向的數據包
memp_free(MEMP_ARP_QUEUE, q); // 釋放記錄下的緩衝鏈表表頭
etharp_send_ip(netif, p, (struct eth_addr*)(netif->hwaddr), ethaddr); // 發送數據包
pbuf_free(p); // 釋放數據包緩存空間
}
#endif
return ERR_OK;
}
從源程序中可以看出,update_arp_entry
的流程如下:先通過調用find_entry
找到對應ipaddr
對應的表項,並設置相應的arp
表項的成員(主要是state, netif, ethaddr, cttime
),最後如果定義了ARP_QUEUEING
,並且這個arp
表項上有未發送的數據包的話,則把這些數據全部發送出去。雖然比較囉嗦,但是還是我們還是根據不同的ipaddr
經過find_entry
執行後,來看看update_arp_entry
運行的幾種不同情況。
首先可以肯定的是,update_arp_entry
的兩個參數ipaddr
和ethaddr
必是互相匹配的,因爲它們是從源主機發來的IP包或ARP包中解析出來的,代表了源主機的MAC地址和IP地址。find_entry
利用ipaddr
作爲參數執行後,返回一個ARP表項索引。如果該表項處於empty
狀態,那麼該表項現在一定是新創建的,此時設置該表項爲stable
狀態並設置該表項其他字段值後即結束。如果該表項是處於pending
狀態,由於此時已經有了和ipaddr
匹配的MAC地址返回,所以該表項也被設置爲stable
狀態並同時設置該表項其他字段值。如果該表項是處於stable
狀態,其實此時只需要將ctime
的值復位即可,但是LWIP爲了節省代碼量,它還是選擇像上面的情況一樣做相同的處理,這樣雖然有些步驟是多餘的,但並不影響函數功能。最後都會檢查該表項是否還有數據需要發送,如果有,則將所有數據包發送出去。
現在是時候從宏觀上來看看到底ARP是怎麼一個工作流程,以及它在整個LWIP協議棧當中發揮的重要作用。。
該圖簡潔明瞭的解釋了基本所有LWIP的數據包接收與發送的全過程。我們可以看到幾個熟悉的身影:etharp_query、etharp_request、update_arp_entry
。在前面已經講過了的!
ARP從功能上來說可以簡單的分成兩個部分:當有數據包輸入時,更新arp表,如果是ip包則遞交給ip層,如果是arp包,則針對不同的arp包類型做相應的響應;當向目的ip發送一個數據包的時候,需要通過arp實現ip到MAC地址的映射,必要時,需要發送廣播數據包獲得目標機器的MAC地址。
LWIP利用netif.input
指向的函數接收以太網數據包,通常這個函數是ethernet_input
。注意,這裏並不是說ethernet_input
直接與底層硬件交互接收數據包,而是更底層的函數接收到數據包後將數據包遞交給ethernet_input
,ethernet_input
再對其進行處理。
以太網的幀類型可以是:IP,ARP,甚至可以是pppoe
, wlan
等。這裏主要分析IP和ARP兩種類型的數據包。ethernet_input
根據以太網首部的類型字段判斷收到的數據包的類型,如果是IP包,則將該包遞交給etharp_ip_input
,如果是ARP包,則將該包遞交給etharp_arp_input
。
對於ip類型的數據包,etharp_ip_input
首先檢查是否開啓了ETHARP_TRUST_IP_MAC
這個選項,如果開啓了就是要用這個幀中的信息和update_arp_entry
函數來更新arp表(利用幀首部的源mac地址和幀數據中ip報文中的源ip地址),然後丟棄以太網幀首部,將IP報文通過ip_input
函數遞交給ip層。
對於arp類型的數據包,etharp_arp_input
函數首先利用數據包頭信息更新arp表的內容,
然後再判斷該ARP數據包的類型,如果是ARP請求包,則首先判斷這個包是不是給自己的,如果是給自己的,則在原有包的基礎上重組一個ARP應答包發送出去(注意此處並沒有重新分配一個pbuf
,而是借用了原來的緩衝結構)。如果不是給自己的,則直接忽略。如果是ARP應答包,主要的工作就是更新arp表,但是這一步已經在arp包剛進來的時候就處理了,所以這裏不需要再重複做,這樣ARP包的處理也完畢。
LWIP利用netif.output
指向的函數發送IP數據包,通常這個函數是etharp_output
。注意,這裏並不是說etharp_output
直接與底層硬件交互發送數據包,而是將數據包做相應的處理,最終遞交給netif.linkoutput
函數來發送的。
etharp_output
函數接收IP層要發送的數據包,並將數據包發送出去。由於是發送ip數據包,所以函數一開始需要增加緩衝區大小,大小爲以太網的數據首部的大小。然後檢查ip地址,可以分爲廣播包,多播包,單播包(單播包又分爲是局域網內部還是局域網外面)。
廣播包:判斷目的IP地址是不是爲全1,或者是全0(老版本中使用的),如果是廣播包則目的IP的MAC地址不需要查詢arp表,直接將MAC地址設置爲全1發送即可,即MAC六個字節值爲0xff,0xff,0xff,0xff,0xff,0xff
。
多播包:判斷目的ip地址是不是d類地址,即0xexxxxxxx
,如果是多播的話,mac地址也是確定的,即將MAC地址01-00-5e-00-00-00
的低23位設置爲IP地址的低23位。對於以上的兩種數據包,etharp_output
直接調用函數etharp_send_ip
將數據包發送出去。
單播包:要比較目的IP和本地IP地址,看是否是局域網內的,不是局域網內的,則將目的IP地址設置爲默認網關的地址,然後再統一調用etharp_query
函數將數據包發送出去,注意這些數據包在這種情況下可能被連接在相關ARP表項的發送鏈表上,等待發送。