第四十七章 lwIP初探
1)實驗平台:正點原子DNESP32S3開發板
2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6
3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659
4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html
5)正點原子官方B站:https://space.bilibili.com/394620890
6)正點原子DNESP32S3開發板技術交流羣:132780729


ESP32-S3是一款集成了Wi-Fi和藍牙功能的微控制器,而lwIP(輕量級IP)是一個為嵌入式系統設計的開源TCP/IP協議棧。通過使用lwIP庫,ESP32-S3可以實現與外部網絡的通信,包括髮送和接收數據包、處理網絡連接等。因此,ESP32-S3是基於lwIP來實現網絡功能的。
章將分為如下幾個部分:
47.1 TCP/IP協議棧是什麼
47.2 lwIP簡介
47.3 WiFi MAC內核簡介
47.4 lwIP Socket編程接口
47.1 TCP/IP協議棧是什麼
TCP/IP協議棧是一系列網絡協議的總和,構成網絡通信的核心骨架,定義了電子設備如何連入因特網以及數據如何在它們之間進行傳輸。該協議採用4層結構,分別是應用層、傳輸層、網絡層和網絡接口層,每一層都使用下一層提供的協議來完成自己的需求。由於大部分時間都在應用層工作,下層的事情不用操心。此外,網絡協議體系本身很複雜龐大,入門門檻高,因此很難搞清楚TCP/IP的工作原理。如果讀者想深入瞭解TCP/IP協議棧的工作原理,建議閲讀《計算機網絡書籍》。
47.1.1 TCP/IP協議棧架構
TCP/IP協議棧是一個分層結構的模型,每一層負責不同的網絡功能。整個協議棧可以被分為四層,從上到下分別是:應用層、傳輸層、網絡層和網絡接口層。
1,應用層:這是最頂層,負責處理特定的應用程序細節。在這一層,用户的數據被處理和解釋。一些常見的應用層協議包括HTTP、FTP、SMTP和DNS等。
2,傳輸層:這一層負責數據包的分割、打包以及傳輸控制,確保數據能夠可靠、有序地到達目的地。主要的傳輸層協議有TCP和UDP。
3,網絡層:負責確定數據包的路徑從源到目的地。這一層的主要協議是IP(InternetProtocol),它負責在主機之間發送和接收數據包。
4,網絡接口層:這是最底層,負責將數據轉換為可以在物理媒介上發送的信號。這一層的協議涉及到如何將數據幀封裝在數據鏈路層,以便在網絡上進行傳輸。
每一層都使用下一層提供的服務,同時對上一層提供服務。這種分層結構使得協議棧更加靈活,易於擴展和維護。不同層次上的協議一起工作,協調數據在計算機網絡中的傳輸,使得不同的計算機能夠相互通信。
需要注意的是,TCP/IP協議棧和傳統的OSI模型並不完全對應。TCP/IP協議棧是一個簡化的模型,強調了實際的協議實現和因特網的實際運作方式。相比之下,OSI模型更加全面和理想化,它提供了一個框架來描述不同系統之間的交互方式。下圖是IOS協議棧與TCP/IP協議棧分層架構的對比圖。

圖47.1.1.1TCP/IP協議棧的分層結構
ISO/OSI分層模型也是一個分層結構,包括七個層次,從上到下分別是:應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層和物理層。雖然ISO/OSI模型為不同的系統之間的通信提供了一個理論框架,但TCP/IP協議棧更側重於實際的協議實現和因特網的實際運作方式。注意:網絡技術的發展並不是遵循嚴格的ISO/OSI分層概念。實際上現在的互聯網使用的是TCP/IP體系結構,有時已經演變成為圖1.1.1.2所示那樣,即某些應用程序可以直接使用IP層,或甚至直接使用最下面的網絡接口層。

圖47.1.1.2TCP/IP體系結構另一種表示方法
無論那種表示方法,TCP/IP模型各個層次都分別對應於不同的協議。TCP/IP協議棧負責確保網絡設備之間能夠通信。它是一組規則,規定了信息如何在網絡中傳輸。其中,這些協議都分佈在應用層,傳輸層和網絡層,網絡接口層是由硬件來實現。如Windows操作系統包含了CBISC協議棧,該協議棧就是實現了TCP/IP協議棧的應用層,傳輸層和網絡層的功能,網絡接口層由網卡實現,所以CBISC協議棧和網卡構建了網絡通信的核心骨架。因此,無論哪一款以太網產品,都必須符合TCP/IP體系結構,才能實現網絡通信。注意:路由器和交換機等相關網絡設備只實現網絡層和網絡接口層的功能。
47.1.2 TCP/IP協議棧的封包和拆包
TCP/IP協議棧的封包和拆包是指在網絡通信中,將數據按照一定的協議和格式進行封裝和解析的過程。
在TCP/IP協議棧中,數據封裝是指在發送端將數據按照協議規定的格式進行打包,以便在網絡中進行傳輸。在應用層的數據被封裝後,會經過傳輸層、網絡層和網絡接口層的處理,最終轉換成可以在物理網絡上傳輸的幀格式。數據封裝的過程涉及到對數據的分段、壓縮、加密等操作,以確保數據能夠可靠、安全地傳輸到目的地,下圖描述的是封包處理流程。

圖47.1.2.1 TCP/IP協議棧的封包
數據拆包是指接收端收到數據後,按照協議規定的格式對數據進行解析和處理,還原出原始的數據。在接收到數據後,接收端會按照協議規定的層次從下往上逐層處理數據,最終將應用層的數據還原出來。數據拆包的過程涉及到對數據的重組、解壓縮、解密等操作,以確保數據能夠被正確地解析和處理,下圖描述的是拆包處理流程。

圖47.1.2.2 TCP/IP協議棧的拆包
需要注意的是,TCP/IP協議棧的封包和拆包過程涉及到多個層次和協議的處理,需要按照協議規定的格式和順序進行操作。在實際應用中,需要根據具體的情況選擇合適的協議和格式來滿足不同的需求。同時,為了保證數據的安全和可靠性,還需要採取相應的加密、壓縮等措施,以避免數據被篡改或損壞
47.2 lwIP簡介
lwIP,全稱為LightweightIP協議,是一種專為嵌入式系統設計的輕量級TCP/IP協議棧。它可以在無操作系統或帶操作系統環境下運行,支持多線程或無線程,適用於8位和32位微處理器,同時兼容大端和小端系統。它的設計核心理念在於保持TCP/IP協議的主要功能同時儘量減少對RAM的佔用。這意味着,儘管它的體積小巧,但它能夠實現完整的TCP/IP通信功能。通常,lwIP只需十幾KB的RAM和大約40K的ROM即可運行,使其成為資源受限的嵌入式系統的理想選擇。lwIP的靈活性使其既可以在無操作系統環境下工作,也可以與各種操作系統配合使用。這為開發者提供了更大的自由度,可以根據具體的應用需求和硬件配置進行優化。無論是在雲台接入、無線網關、遠程模塊還是工控控制器等場景中,lwIP都能提供強大的網絡支持。
一、lwIP特性參數
lwIP的各項特性,如下表所示:

表47.2.1lwIP基本特性
二、lwIP與TCP/IP體系結構的對應關係

圖 47.2.1 lwIP與TCP/IP體系結構的對應圖解
從上圖可以清晰地看到,lwIP軟件庫主要實現了TCP/IP體系結構中的三個層次:應用層、傳輸層和網絡層。這些層次共同處理和傳輸數據包,確保了數據在網絡中的可靠傳輸。然而,網絡接口層作為TCP/IP協議棧的最底層,其功能並無法通過軟件方式完全實現。這一層的主要任務是將數據包轉換為光電模擬信號,以便能夠在物理媒介上傳輸。這個過程涉及到與硬件的直接交互,包括數據的調製解調、信號的轉換等,這些都是軟件難以模擬或實現的。因此,雖然lwIP軟件庫沒有實現網絡接口層的功能,但通過與底層硬件的緊密配合,它仍然能夠提供完整且高效的TCP/IP通信功能。這也使得lwIP成為一個適用於資源受限的嵌入式系統的理想選擇。
在開發過程中,開發者需要根據具體的硬件平台和需求進行相應的配置和優化,以確保lwIP能夠與硬件完美配合,發揮出最佳的性能。
47.3 WiFi MAC內核簡介
ESP32-S3 完全遵循802.11b/g/n Wi-Fi MAC協議棧,支持分佈式控制功能(DCF)下的基本服務集(BSS)STA和SoftAP操作。支持通過最小化主機交互來優化有效工作時長,以實現功耗管理。ESP32-S3 Wi-Fi MAC支持4個虛擬Wi-Fi接口,同時支持基礎結構型網絡、SoftAP模式和Station+SoftAP混雜模式。它還具備RTS保護、CTS保護、立即塊確認、分片和重組、TX/RX A-MPDU和TX/RX A-MSDU等高級功能。此外,ESP32-S3還支持無線多媒體、GCMP、CCMP、TKIP、WAPI等安全協議,並提供自動Beacon監測和802.11mc FTM支持。
關於ESP32的WiFi MAC內核,官方沒有提供更多學習資料。讀者只需瞭解它扮演TCP/IP協議的網絡接口層角色即可。下面作者使用一張示意圖來介紹WiFi通訊示意圖,如下圖所示。

圖47.3.1 ESP32-S3網絡層次示意圖
從上圖可以看出,ESP32-S3芯片內置WiFi MAC內核。當我們發送數據到網絡時,數據首先被轉化為無線信號,然後發送到該設備連接的WiFi路由器中。接着,路由器通過網線將數據傳輸到目標主機,從而完成數據傳輸操作。以下是作者對於無線網絡傳輸的描述。
1,數據轉化為無線信號:當ESP32-S3想要發送數據到網絡時,它首先會將數據封裝到一個無線傳輸幀中。這一過程涉及到將數據轉化為可以在無線介質上傳輸的格式。
2,發送到WiFi路由器:封裝後的無線信號然後被髮送到ESP32-S3連接的WiFi路由器。WiFi路由器充當一箇中間設備,負責將無線信號轉換為有線網絡信號(如果目標主機是通過有線網絡連接的)或直接轉發無線信號(如果目標主機也是通過WiFi連接的)。
3,路由器傳輸數據:WiFi路由器接收到無線信號後,會進一步處理它。如果目標主機是通過有線網絡連接的,路由器會將無線信號轉換為有線網絡信號,並通過網線將其傳輸到目標主機。如果目標主機也是通過WiFi連接的,路由器會直接轉發無線信號到目標主機。
4,完成數據傳輸:最終,目標主機接收到路由器發送的有線網絡信號或無線信號,並將其解析為原始數據。這樣,整個數據傳輸過程就完成了。
在整個過程中,ESP32-S3的WiFi MAC內核起着核心的作用,它負責管理無線連接、封裝和解封裝數據以及與WiFi路由器進行通信。
47.4 lwIP Socket編程接口
lwIP作者為了方便開發者將其他平台上的網絡應用程序移植到lwIP上,並讓更多開發者快速上手lwIP,作者設計了三種應用程序編程接口:RAW編程接口、NETCONN編程接口和Socket編程接口。然而,由於RAW編程接口只能在無操作系統環境下運行,因此對於內嵌FreeRTOS操作系統的ESP32來説,無法使用這個編程接口。儘管Socket編程接口是由NETCONN編程接口封裝而成,但是該接口非常簡易的實現網絡連接(作者推薦使用此接口)。需要注意的是,由於受到嵌入式處理器資源和性能的限制,部分Socket接口並未在lwIP中完全實現。因此,為了實現網絡連接,推薦使用Socket API。
下面作者簡單介紹一下lwIP Socket編程接口常用的API函數。這些API函數如下所示。
(1) socket函數
該函數的原型,如下源碼所示:
#define socket(domain,type,protocol) lwip_socket(domain,type,protocol)
向內核申請一個套接字,本質上該函數調用了函數lwip_socket,該函數的參數如下表所示:

表47.4.1函數Socket()相關形成描述
(2) bind函數
該函數的原型,如下源碼所示:
#define bind(s,name,namelen) lwip_bind(s,name,namelen)
int bind(int s, const struct sockaddr *name, socklen_t namelen)
該函數與netconn_bind函數一樣,用於服務器端綁定套接字與網卡信息,本質上就是對函數netconn_bind再一次封裝,從上述源碼可以知道參數name指向一個sockaddr結構體,它包含了本地IP地址和端口號等信息;參數namelen指出結構體的長度。結構體sockaddr定義如下源碼所示:
struct sockaddr {
u8_t sa_len; /* 長度 */
sa_family_t sa_family; /* 協議簇 */
char sa_data[14]; /* 連續的 14 字節信息 */
};
struct sockaddr_in {
u8_tsin_len; /* 長度 */
u8_tsin_family; /* 協議簇 */
u16_tsin_port; /* 端口號 */
struct in_addr sin_addr; /* IP地址 */
char sin_zero[8];
};
可以看出,lwIP作者定義了兩個結構體,結構體sockaddr中的sa_family指向該套接字所使用的協議簇,本地IP地址和端口號等信息在sa_data數組裏面定義,這裏暫未用到。由於sa_data以連續空間的方式存在,所以用户要填寫其中的IP字段和端口port字段,這樣會比較麻煩,因此lwIP定義了另一個結構體sockaddr_in,它與sockaddr結構對等,只是從中抽出IP地址和端口號port,方便於用於的編程操作。
(3) connect函數
該函數與netconn接口的netconn_connect函數作用是一樣的,因此它是被netconn_connect函數封裝了,該函數的作用是將Socket與遠程IP地址和端口號綁定,如果開發板作為客户端,通常使用這個函數來綁定服務器的IP地址和端口號,對於TCP連接,調用這個函數會使客户端與服務器之間發生連接握手過程,並建立穩定的連接;如果是UDP連接,該函數調用不會有任何數據包被髮送,只是在連接結構中記錄下服務器的地址信息。當調用成功時,函數返回0;否則返回-1。該函數的原型如下源碼所示:
#define connect(s,name,namelen) lwip_connect(s,name,namelen)
int lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);
(4) listen函數
該函數和netconn的函數netconn_listen作用一樣,它是由函數netconn_listen封裝得來的,內核同時接收到多個連接請求時,需要對這些請求進行排隊處理,參數backlog指明瞭該套接字上連接請求隊列的最大長度。當調用成功時,函數返回0;否則返回-1。該函數的原型如下源碼所示:
#define listen(s,backlog) lwip_listen(s,backlog)
int lwip_listen(int s, int backlog);
注意:該函數作用於TCP服務器程序。
(5) accept函數
該函數與netconn_accept作用是一樣的,當接收到新連接後,連接另一端(客户端)的地址信息會被填入到地址結構addr中,而對應地址信息的長度被記錄到addrlen中。函數返回新連接的套接字描述符,若調用失敗,函數返回-1。該函數的原型如下源碼所示:
#define accept(s,addr,addrlen) lwip_accept(s,addr,addrlen)
int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);
注意:該函數作用於TCP服務器程序。
(6) send()/sendto()函數
該函數是被netconn_send封裝的,其作用是向另一端發送UDP報文,這兩個函數的原型如下源碼所示:
#define send(s,dataptr,size,flags) lwip_send(s,dataptr,size,flags)
#definesendto(s,dataptr,size,flags,to,tolen) lwip_sendto(s,dataptr,size,flags,to,tolen)
ssize_t lwip_send(int s, const void *dataptr, size_t size, int flags);
ssize_t lwip_sendto(int s, const void *dataptr, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
可以看出,函數sendto比函數send多了兩個參數,該函數如下表所示:

表47.4.2 函數sendto()和send()形參描述
(7) write函數
該函數用於在一條已經建立的連接上發送數據,通常使用在TCP程序中,但在UDP程序中也能使用。該函數本質上是基於前面介紹的send函數來實現的,其參數的意義與send也相同。當函數調用成功時,返回成功發送的字節數;否則返回-1。
(8) read()/recv()/recvfrom()函數
函數recvfrom和recv用來從一個套接字中接收數據,該函數可以在UDP程序使用,也可在TCP程序中使用。該函數本質上是被函數netconn_recv的封裝,其參數與函數sendto的參數完全相似,如下表所示,數據發送方的地址信息會被填寫到from中,fromlen指明瞭緩存from的長度;mem和len分別記錄了接收數據的緩存起始地址和緩存長度,flags指明用户控制接收的方式,通常設置為0。兩個函數的原型如下源碼所示:
#define recv(s,mem,len,flags) lwip_recv(s,mem,len,flags)
#definerecvfrom(s,mem,len,flags,from,fromlen) lwip_recvfrom(s,mem,len,flags,from,fromlen)
ssize_t lwip_readv(int s, const struct iovec *iov, int iovcnt);
ssize_t lwip_recvfrom(int s, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
#define read(s,mem,len) lwip_read(s,mem,len)
ssize_t lwip_read(
int s,
void *mem, size_t len);

表47.4.3 函數recv()和recvfrom()形參描述
(9) close函數
函數close作用是關閉套接字,對應的套接字描述符不再有效,與描述符對應的內核結構lwip_socket也將被全部復位。該函數本質上是被netconn_delete的封裝,對於TCP連接來説,該函數將導致斷開握手過程的發生。若調用成功,該函數返回0;否則返回-1。該函數的原型如下源碼所示:
#define close(s) lwip_close(s)
int lwip_close(int s);