1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729
第四十七章 lwIP初探
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(Internet Protocol),它负责在主机之间发送和接收数据包。
4,网络接口层:这是最底层,负责将数据转换为可以在物理媒介上发送的信号。这一层的协议涉及到如何将数据帧封装在数据链路层,以便在网络上进行传输。
每一层都使用下一层提供的服务,同时对上一层提供服务。这种分层结构使得协议栈更加灵活,易于扩展和维护。不同层次上的协议一起工作,协调数据在计算机网络中的传输,使得不同的计算机能够相互通信。
需要注意的是,TCP/IP协议栈和传统的OSI模型并不完全对应。TCP/IP协议栈是一个简化的模型,强调了实际的协议实现和因特网的实际运作方式。相比之下,OSI模型更加全面和理想化,它提供了一个框架来描述不同系统之间的交互方式。下图是IOS协议栈与TCP/IP协议栈分层架构的对比图。
图47.1.1.1 TCP/IP协议栈的分层结构
ISO/OSI分层模型也是一个分层结构,包括七个层次,从上到下分别是:应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。虽然ISO/OSI模型为不同的系统之间的通信提供了一个理论框架,但TCP/IP协议栈更侧重于实际的协议实现和因特网的实际运作方式。注意:网络技术的发展并不是遵循严格的ISO/OSI分层概念。实际上现在的互联网使用的是TCP/IP体系结构,有时已经演变成为图1.1.1.2所示那样,即某些应用程序可以直接使用IP层,或甚至直接使用最下面的网络接口层。
图47.1.1.2 TCP/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,全称为Lightweight IP协议,是一种专为嵌入式系统设计的轻量级TCP/IP协议栈。它可以在无操作系统或带操作系统环境下运行,支持多线程或无线程,适用于8位和32位微处理器,同时兼容大端和小端系统。它的设计核心理念在于保持TCP/IP协议的主要功能同时尽量减少对RAM的占用。这意味着,尽管它的体积小巧,但它能够实现完整的TCP/IP通信功能。通常,lwIP只需十几KB的RAM和大约40K的ROM即可运行,使其成为资源受限的嵌入式系统的理想选择。lwIP的灵活性使其既可以在无操作系统环境下工作,也可以与各种操作系统配合使用。这为开发者提供了更大的自由度,可以根据具体的应用需求和硬件配置进行优化。无论是在云台接入、无线网关、远程模块还是工控控制器等场景中,lwIP都能提供强大的网络支持。
一、lwIP特性参数
lwIP的各项特性,如下表所示:
表47.2.1 lwIP基本特性
二、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_t sin_len; /* 长度 */
- u8_t sin_family; /* 协议簇 */
- u16_t sin_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)
- #define sendto(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)
- #define recvfrom(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);
复制代码
表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);
复制代码
|