图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,该函数的参数如下表所示:
参数 | 描述 |
| 创建的套接字指定使用的协议簇 |
AF_INET 表示IPv4网络协议 |
AF_INET6 表示IPv6 |
AF_UNIX 表示本地套接字(使用一个文件) |
| 协议簇中的具体服务类型 |
SOCK_STREAM(可靠数据流交付服务,比如TCP) |
SOCK_DGRAM(无连接数据报交付服务,比如UDP) |
SOCK_RAW (原始套接字,比如RAW) |
| 实际使用的具体协议(常见的有IPPROTO_TCP、IPPROTO_UDP等,若设置为"0",表示根据前两个参数使用缺省协议) |
表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多了两个参数,该函数如下表所示:
参数 | 描述 |
s | Socket接口 |
dataptr | 发送数据的起始地址 |
size | 长度 |
flags | 数据发送时的特殊处理,例如带外数据、紧急数据等,通常设置为0 |
to(sendto()) | 目的地址信息 |
tolen(sendto()) | 信息的长度 |
表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);
#define read(s,mem,len) lwip_read(s,mem,len)
ssize_t lwip_read(
int s,
void *mem, size_t len);
参数 | 描述 |
s | Socket接口 |
mem | 接收数据的缓存起始地址 |
len | 缓存长度 |
flags | 用户控制接收的方式,通常设置为0 |
from(recvfrom()) | 发送方的地址信息 |
fromlen(recvfrom()) | 缓存from的长度 |
表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);