協議
lwIP
是一模塊化的框架,支持很多的協議,大部分代碼可以為了精簡代碼刪除。
鏈路與網絡協議
ARP: 地址解析協議
地址解析協議 ARP: Address Resolution Protocol
是鏈路層協議,用來轉換本機硬件地址 (即 MAC
地址) 與 IP
地址。
支持 ARP
的網絡接口會令 etharp_output
處理所有外發的包,並將會在它的 netif
結構體中設置 flag
標識來使能免費 ARP
(gratuitous ARP
)。並將維護一個最近的 IP
地址以及相關的硬件地址表,如果外發的包不能匹配任何已知的硬件地址,代碼將會執行一個 ARP
請求來解析正確的地址。
一個應用程序員不應該直接調用這個協議,因為如果一切工作正常,那么這個過程是透明的。有幾個標識能夠確定 ARP
表的大小、包是如何在掛起的 ARP
請求后排隊的,以及 ARP
代碼是否需要從到來的包中學習 MAC-IP
地址綁定關系。這些選項可以在 lwipopts.h
頭文件中配置。
IPv4
現今使用的主流的網絡層協議。
IPv4
(Internet Protocol version 4) 是現今廣為使用的網絡協議。它具有如下特性:
- 盡可能發送,網絡設備盡可能將要發送的包發送到目標點,然而不能確保百分百完成,所有的中間媒介都會盡可能完成這個任務
- 不能保證發送或重傳質量,目標點不能通知主機它接受到了數據包
- 接受到包的順序可能與發送包的順序不同,一旦包發送到網絡上,它們可能會經過完全不同的路由。因此包到達目標點的順序是不能保證的
- 主機可能會收到重復的包
針對應用向的 IPv4
操作地址
一個 IPv4
地址由 32
比特位長的字串組成,可以通過 .
划分成格式 127.0.0.1
。在 lwIP
中,IP
地址由 struct ip_addr
結構體持有,它持有無符號的 32
比特位長的子。因此,設置一個 IP
地址,可以使用如下的代碼:
#include <lwip/ip_addr.h>
struct ip_addr local;
IP4_ADDR(&local, 127.0.0.1); // 設置回環地址為 127.0.0.1
其他會實用到 ip_addr
結構體的如下:
IP_ADDR_ANY
任意地址,比如,如果你想要監聽一個TCP
端口,但是不希望綁定到一個指定的地址ip_addr_set(dest, src)
從一個結構體復制地址到另一個結構體ip_addr_cmp(addr1, addr2)
比較兩個地址是否相同ip4_addr1(ipaddr)
IP
地址的第一個字節,比如192.168.133.144
中的192
ip4_addr2(ipaddr)
IP
地址的第二個字節,比如192.168.133.144
中的168
ip4_addr3(ipaddr)
IP
地址的第三個字節,比如192.168.133.144
中的133
ip4_addr4(ipaddr)
IP
地址的第四個字節,比如192.168.133.144
中的144
主機與網絡字節順序
因為大端、小端的架構不同,必須確定結構體中的值是按照主機中的字序排布的還是網絡字節序排布的。
網絡字節序是以大端排布的。你的主機平台可能也是大端,這樣你的代碼無需考慮這些技術細節。但是,如果你為一台小端處理器 (比如 Intel
,AMD
,ARM
等) 重新編譯了你的程序,那么你的代碼可能會出現一些難以確定的問題。
因此,下面的一些主機到網絡的字節轉換函數可能會有幫助,這些函數如下:
netval = htonl(hostval)
將32
比特位主機字序轉換為網絡字序netval = htons(hostval)
將16
比特位主機字序轉換為網絡字序hostval = ntohl(netval)
將32
比特位網絡字序轉換為主機字序hostval = ntohs(netval)
將16
比特位網絡字序轉換為主機字序
lwIP
在大部分情況下,會自動為你注意所有的內部網絡協議結構體。在你手動設置或比較 IP
地址時,需要注意這一點;或者,如果你將結構體串起到包中,並發送到另一個系統上時需要注意這一點。如果你的目標的字序與你主機的字序不同,那么你必須按照既定的標准,保證你這邊的字序與對方的字序一致(你這邊的16位與對方的16位一致)。
注意一點,ip_addr->addr
是網絡字序排布的,u32_t someip
是主機字序排布的。
IN_CLASSA
宏檢查一個IP
地址是否是一個A
類地址,它操作32
比特值,因此是主機字節序IP_ADDR_ANY
宏是struct ip_addr
結構體,它的IP
地址為網絡字節序ip_addr_cmp
宏比較兩個保存為struct ip_addr
中的IP
地址,因此它是網絡字節序
下面一些實例:
struct ip_addr ip, otherip;
ip.addr = 0x7f000001; /* BAD,大端下為 127.0.0.1,小端下為 1.0.0.127 */
IP4_ADDR(&ip, 127.0.0.1); /* GOOD */
ip_addr_set(&otherip, &ip); /* GOOD,網絡字序分配給網絡字序 */
otherip.addr = ip.addr; /* BAD,可以工作,但是最好實用宏 */
if(ip.addr == 0x7f000001); /* BAD,在大端上 true,小端上 false */
if(ip.addr == 0x0100007f); /* BAD,在小端上 true,大端上 false */
if(ntohl(ip.addr) == 0x7f000001); /* GOOD */
if(ip_addr_cmp(&ip, &otherip)); /* GOOD */
為接口分配地址
一個網絡接口只能具有一個 IP
地址。在 lwIP
中,支持三種方法為接口分配 IP
地址:
- 靜態
IP
:netif
的IP
地址可以通過netif_add
或netif_set_addr
或netif_set_ipaddr
進行初始化 DHCP
動態獲取:DHCP
是一個可選協議,可從DHCP
服務器上獲取動態IP
AUTOIP
:AUTOIP
是一個可選協議,可以從本地子網中挑選一個IP
地址,而不需要通過服務器為其分配地址
IPv6
IPv4
的后繼,將 IP
地址擴展為 128
比特。
應用角度的 IPv6
對 IPv6
的支持現在已經添加到 lwIP
中。在 v1.4.x
以上版本的 lwIP
可以在 IPv4
或 IPv6
之間進行選擇,但不是兩者同時使用。雙協議棧的使用是當前開發版本中的一個特性,可能會在 v1.5.0
版本發布。
ICMP
IP
的控制協議。
ICMP 支持
ICMP
應用支持三個協議:
Echo Reply
或ping
,客戶端響應ping
請求,響應IP
包中的數據- 目的地不可達,指示設備不能轉發
IP
數據包。比如,如果一個包尋址到的設備需求一個本設備不支持的協議,就會報這個錯誤 - 超時,指示設備因為包的生命周期
Time To Live TTL
到達0
,而丟棄了數據包
應用角度的 ICMP
ICMP
信息由 lwIP
協議棧自身處理或生成。因此一個應用不應該與 ICMP
的代碼進行交互。如果用戶希望生成自己的 ping
,那么可以組織一個包作為 IP
包,通過 IP
模塊進行發送。
IGMP
以 IP
進行的多播協議。
傳輸層協議
UDP
沒有可靠握手機制的套接字協議。
TCP
具有握手,可以進行流控的協議。
其他
DHCP
在帶有服務器時的 IP
地址獲取方法。
應用角度的 DHCP
為了使能 DHCP
,你必須確保編譯、鏈接到 DHCP
。你可以通過在 lwipopts.h
頭文件中將 LWIP_DHCP
值設置為 1
來使能這一功能,這會添加指向 dhcp
結構體的字段到 netif
中。dhcp
結構體會在 dhcp_start()
中進行分配。另外,LWIP_UDP
必不能設置為 0
,因為 DHCP
是運行在 UDP
上的協議。
在一個簡單的設置中,在初始化接口之后,可以簡單的調用:
dhcp_start(&mynetif);
之后,為了正確處理動態 IP
使用時限,DHCP
需要調用一系列的計時函數。在 1.4.0
版本之后,你只需要調用一個單獨的函數,來處理協議棧中的所有的定時器,因此添加下面的函數到你的 main
循環中是等效的:
sys_check_timeouts();
之后,你需要檢查你的接口 ->dhcp->state == DHCP_BOUND
,就可以了。
對於 lwIP 2.0
,你需要調用:
dhcp_supplied_address(const struct netif *netif)
如果需要處理比較復雜的網絡變化環境,比如,一個移動設備可能會不停的更換鏈接到的網絡,那么你需要告知 DHCP
函數這一情況。這通常可以通過調用 dhcp_network_changed()
函數實現。因為協議棧中的 AUTOIP
以及 IGMP
協議也需要關注這個情況,所以正確的調用方法是:
netif_set_link_up(&mynetif);
netif_set_link_down(&mynetif);
這兩個函數需要與接口的 link
狀態變化綁定。
下面是詳細信息,為了在一個接口上使用 DHCP
,簡單實用如下函數:
dhcp_start()
為一個接口啟動DHCP
配置dhcp_renew()
強制刷新之前的時限dhcp_release()
釋放DHCP
時限,通常在dhcp_stop()
之前調用dhcp_stop()
為一個接口停止DHCP
配置dhcp_inform()
告知服務器我們的IP
地址
注意: 這些函數是 lwIP
的內核函數。它並不會受到並發訪問的保護。在多線程環境中,它們可能只在核心線程調用 (即,tcp-ip
線程)。在其他線程調用時,使用在 netifapi.c
中定義的對應版本的 netifapi_dhcp_*()
函數。
一個選擇是利用 PHY
自協商的特點。大部分 PHY
在連接狀態 link
發生改變時生成中斷。對 PHY
連接狀態發生改變的中斷處理傳遞給 tcp-ip
線程。之后在 HandlePhyInterrupt
,如果 link
是 up
態,進行任何必要的硬件寄存器調整,來適配子協商的速度,之后調用 dhcp_start
。如果 link
是 down
態,首先調用 netif_set_down()
,前面提到的其他的 dhcp
函數可能就不需要了。
AUTOIP
沒有服務器時的 IP
地址選擇方法。
應用角度的 AUTOIP
為了使能 AUTOIP
,需要配置 lwipopts.h
頭文件中的 LWIP_AUTOIP
。
在一個接口上使用 AUTOIP
,簡單實用如下命令:
autoip_start()
使用一個新的IP
另接口up
autoip_stop()
使接口down
注意: 這些函數是 lwIP
的內核函數。不會受到並發訪問的保護。在多線程的環境下,工作類似 DHCP
。
SNMP
用來監控網絡條件。
PPP
用來在兩個節點之間直接創建鏈接。
應用角度的 PPP
有兩種使用 PPP
的方式,一種是通過 PPPoE
(PPP over Ethernet
),一種是 PPP over serial
,lwIP
支持在線程環境下運行,這個情況下 PPP
是一個單獨的任務,與 lwIP
主線程相分離。lwIP
也支持在 main
循環中運行,在 main
函數中調用 lwIP
的函數。
串行下的 PPP
為了設置 PPP
通過串行鏈接,你需要提供串行 IO
功能。
無論是線程環境還是 main
循環函數環境,都需要應用 sio_write()
。
/**
* Writes to the serial device.
*
* @param fd serial device handle
* @param data pointer to data to send
* @param len length (in bytes) of data to send
* @return number of bytes actually sent
*
* @note This function will block until all data can be sent.
*/
u32_t sio_write(sio_fd_t fd, u8_t *data, u32_t len);
任務支持
除了 sio_write()
,還需要應用 sio_read()
、sio_read_abort()
以及 linkStatusCB
。前兩個函數是靜態鏈接的,最后一個函數通過函數指針定義 (因此名稱可以不同,也可以動態創建)。
函數 sio_read()
在 pppInputThread
中重復調用。它會阻塞等待,直到超時或請求緩沖區被填充,會從串行設備上讀取數據。如果調用了 sio_read_abort()
那么必須廢棄掉 sio_read()
。在 sio_read
與 sio_read_abort
之間這樣的軟連接關系必須被開發者自己實現,可以通過全局變量實現,可以通過 RTOS
的事件實現等。超時時長需要較小,大小只需要數據足夠有 1
字節寬度滿足 PPP
協議棧響應即可。2
毫秒的時長就已經足夠了。
/**
* Reads from the serial device.
*
* @param fd serial device handle
* @param data pointer to data buffer for receiving
* @param len maximum length (in bytes) of data to receive
* @return number of bytes actually received - may be 0 if aborted by sio_read_abort
*
*/
u32_t sio_read(sio_fd_t fd, u8_t *data, u32_t len);
sio_read_abort()
函數必須要讓 sio_read
馬上退出。這是在 tcpip_thread
中實現的,大部分情況是在結束一個 PPP
會話時執行 (終止、鏈接斷開或顯式關閉)。
/**
* Reads from the serial device.
*
* @param fd serial device handle
* @param data pointer to data buffer for receiving
* @param len maximum length (in bytes) of data to receive
* @return number of bytes actually received - may be 0 if aborted by sio_read_abort
*
*/
u32_t sio_read(sio_fd_t fd, u8_t *data, u32_t len);
回調函數
下面的回調函數:
void (*linkStatusCB)(void *ctx, int errCode, void *arg)
會在下面事件下調用:
- 鏈接終止,
errCode
非0
,arg
為空 - 接口
up
,errCode
為PPPERR_NONE
,arg
是指向ppp_addrs
結構體,這個結構體中包含IP
地址 - 接口
down
,errCode
為PPPERR_CONNECT
,arg
為空
errCode
可以為下面的情況:
#define PPPERR_NONE 0 /* 無錯誤 */
#define PPPERR_PARAM -1 /* 無效參數 */
#define PPPERR_OPEN -2 /* 不能開啟 PPP 會話 */
#define PPPERR_DEVICE -3 /* 無效的 IO 設備 */
#define PPPERR_ALLOC -4 /* 不能分配資源 */
#define PPPERR_USER -5 /* 用戶打斷 */
#define PPPERR_CONNECT -6 /* 鏈接丟失 */
#define PPPERR_AUTHFAIL -7 /* 認證失敗 */
#define PPPERR_PROTOCOL -8 /* 協議不匹配 */
ctx
指針是用戶可選定義的指針,是調用 pppOverSerialOpen
函數的參數,可以指向用戶定義的數據。
無任務支持
你需要在你的主線程中接收串行數據,之后你的主線程調用:
pppos_input(int pd, u_char *data, int len);