ESP32WIFI-2.TCP协议
说明:
- 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。
- 本文档主要介绍ESP32的TCP协议,本节内容实现TCP客户端和服务端。
- 此文档仅作为个人学习笔记,本人不能保证其绝对准确性。
修订历史:
文档名称 | 版本 | 作者 | 时间 | 备注 |
---|---|---|---|---|
ESP32网络入门-TCP协议 | v1.0.0 | DuRuofu | 2024-03-15 | 首次建立 |
ESP32网络入门-TCP协议
一、介绍
在开始使用TCP协议之前,我们需要掌握一些基本的概念和前置知识:
最基本的一点:TCP/UDP工作在网络OSI的七层模型中的第四层——传输层,IP在第三层——网络层,WIFI(狭义上)在一二层-物理层和数据链路层。
1.1 套接字(socket)
下面的部分搬运自:Socket介绍 (如有侵权,请联系作者删除)
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。
网络协议是很复杂的,它的硬件接口可以是WIFI,网线,4G网卡等,我们开发网络程序,不可能亲自去了解这些物理层,链路层的网络协议和实现。我们通过抽象出统一的上层建筑(Socket)来完成代码编写,这样无论底层(链路层,网络层)是何种形式,我们需要考虑的东西都是相同的(Socket的概念是一样的)。
socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
1.2 C/S模式
C/S分布式模式,是计算机用语。C是指Client,S是指Server,C/S模式就是指客户端/服务器模式。是计算机软件协同工作的一种模式,通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。
1.3 TCP协议
请自行了解
二、使用
本节的工程基于ESP32WIFI-1.WIFI连接中的wifi_sta历程
下图展示了TCP协议服务端和客户端的基本流程:
2.1 TCP客户端
客户端程序流程:初始化-连接-数据交换-断开连接
graph LR;
A(Initialize) --> B(Connect);
B --> C(Communicate);
C --> D(Disconnect);
```
#### 2.1.1 准备工作
准备工作主要是连接wifi,为下面的网络协议提供支持,可以参考:[ESP32WIFI-1.WIFI连接](https://www.duruofu.top/2024/03/15/4.%E7%A1%AC%E4%BB%B6%E7%9B%B8%E5%85%B3/MCU/ESP32/05.ESP32WIFI%E5%85%A5%E9%97%A8/5.1-ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/)
#### 2.1.2 创建套接字
使用函数`int socket(int domain,int type,int protocol)`创建套接字,参数分别为
- `domain`:指定协议家族或地址族,常用的有 `AF_INET`(IPv4 地址族)和 `AF_INET6`(IPv6 地址族)。
- `type`:指定套接字类型,常见的有 `SOCK_STREAM`(流套接字,提供面向连接的、可靠的数据传输)和 `SOCK_DGRAM`(数据报套接字,提供无连接的、不可靠的数据传输)。
- `protocol`:指定协议,一般为 0,默认由 `socket()` 函数根据前两个参数自动选择合适的协议。
```c
// 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) // 创建失败返回-1
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
return;
}
2.1.3 配置并连接服务端
使用函数connect(int s,const struct sockaddr *name,socklen_t namelen)
连接服务端,参数分别为:
s
: 表示一个已经创建并绑定到本地地址的套接字描述符。name
: 是一个指向目标服务器地址结构体的指针,通常是struct sockaddr
结构体或其派生结构体,用来指定要连接的远程服务器的地址信息。namelen
: 表示参数name
指向的地址结构体的长度。
这里的 struct sockaddr
结构体用于配置IP协议,这里以IPV4为例,参数如下:
sin_len
: 该字段表示结构体的长度,单位为字节。在这个结构体中,用一个字节来表示结构体的长度。sin_family
: 这是一个表示地址族(Address Family)的字段,用于指示地址的类型,如IPv4或IPv6。在这里,用sa_family_t
类型来表示,可能是一个枚举值或整数值,用于指示IPv4地址族。(和上一步的domain
参数相同)sin_port
: 一个16位的整数,表示端口号。in_port_t
类型通常被定义为一个16位的整数,用于存储端口号。sin_addr
: 一个struct in_addr
类型的结构体,用于存储IPv4地址信息。通常struct in_addr
包含一个32位的整数,表示IPv4地址。
代码如下:
1 |
|
2.1.4 发送消息
使用send(int s,const void *dataptr,size_t size,int flags)
函数发送消息,参数为:
s
:指定的套接字描述符,即要发送消息的目标套接字。dataptr
:指向要发送数据的指针,可以是任意类型的数据。size
:要发送的数据大小,以字节为单位。flags
:用于指定发送操作的附加选项,通常可以设为0。
例如:
1 |
|
2.1.5 接收消息
使用recv(int s,void *mem,size_t len,int flags)
接收数据,参数为:
s
:指定要接收数据的套接字描述符。mem
:指向存放接收数据的缓冲区的指针。len
:表示接收缓冲区的长度。flags
:指定接收操作的附加选项,通常可以设置为 0。
例如:
1 |
|
完整程序请看下面第三部分:
2.1 TCP服务器
2.2.1 准备工作
初始化NVS、 连接WIFI
2.2.2 创建并配置socket
1 |
|
这里使用了一个用于设置 socket 属性,用函数 setsockopt()
,函数原形如下:
1 |
|
sockfd
:指定要设置选项的套接字文件描述符。level
:指定选项的协议级别。常见的级别包括SOL_SOCKET
(通用套接字选项)和IPPROTO_TCP
(TCP 协议选项)等。optname
:指定要设置的选项名称,可以是下列之一或者协议特定的选项。常见的选项包括SO_REUSEADDR
(允许地址重用)、SO_KEEPALIVE
(启用连接保活)、SO_RCVBUF
(设置接收缓冲区大小)等。optval
:指向包含选项值的缓冲区的指针。optlen
:指定选项值的长度。
2.2.3 配置服务器信息
代码:
1 |
|
bind
函数用于将一个套接字与一个地址(通常是 IP 地址和端口号)绑定在一起,以便在该地址上监听连接或发送数据。它的原型如下:
1 |
|
sockfd
:指定要绑定地址的套接字文件描述符。addr
:指向一个sockaddr
结构体的指针,该结构体包含了要绑定的地址信息。在 IPv4 地址族中,可以使用sockaddr_in
结构体;在 IPv6 地址族中,可以使用sockaddr_in6
结构体。通常,你需要将地址信息转换为sockaddr
结构体的形式,然后传递给bind
函数。addrlen
:指定地址结构体的长度。
2.2.4 监听客户端连接
1 |
|
listen()
函数用于将一个套接字(通常是服务器套接字)转换为被动套接字,即用于接受连接请求
sockfd
:指定要监听的套接字文件描述符。backlog
:指定在内核中排队等待接受连接的最大连接数。这个参数限制了同时等待连接的数量,超过这个数量的连接请求将被拒绝。这并不是一个限制同时连接的数量,而是限制等待连接队列的长度。listen()
函数在成功时返回 0,失败时返回 -1
2.2.4 建立接收
1 |
|
sockaddr_storage
是一个足够大的结构体,可用于存储任意地址族(IPv4 或 IPv6)的地址信息。
建立连接使用函数accept()
,它用于接受传入的连接请求,并创建一个新的套接字来与客户端进行通信。它的原型如下:
1 |
|
参数:
sockfd
:指定正在监听连接请求的套接字文件描述符。addr
:用于存储客户端地址信息的指针。当有连接请求到达时,accept()
函数会将客户端的地址信息填写到这个结构体中。addrlen
:指向一个整数的指针,表示传入的地址结构体的长度。在调用accept()
函数之前,必须将这个参数设置为addr
缓冲区的大小。当accept()
函数返回时,这个参数会更新为实际填充到addr
缓冲区中的地址结构体的长度。
建立连接成功后,通过调用 setsockopt()
函数,设置了套接字的 Keep-Alive 选项,以确保连接保持活跃状态。
SO_KEEPALIVE
:启用或禁用 TCP Keep-Alive 机制。TCP_KEEPIDLE
:设置 TCP Keep-Alive 空闲时间,即连接空闲多长时间后开始发送 Keep-Alive 消息。TCP_KEEPINTVL
:设置 TCP Keep-Alive 消息的发送间隔,即两次 Keep-Alive 消息之间的时间间隔。TCP_KEEPCNT
:设置 TCP Keep-Alive 消息的发送次数,即发送多少次 Keep-Alive 消息后仍未收到响应才认为连接失效。
2.2.6 接收/发送数据
接收和发送依然使用recv
和send
,下面实现了一个简单的数据接收,回传函数,参数为建立连接的套接字。
1 |
|
程序解释如下:
recv()
函数用于从套接字接收数据,并将接收到的数据存储在rx_buffer
中。它的参数包括套接字文件描述符sock
、接收缓冲区rx_buffer
、缓冲区大小以及一些可选的标志参数。如果recv()
返回值小于 0,则表示出现了错误;如果返回值为 0,则表示连接已关闭;否则,返回接收到的字节数。- 如果接收到的字节数小于 0,表示发生了接收错误,这时会记录错误信息到日志。
- 如果接收到的字节数为 0,表示连接已关闭,这时会记录警告信息到日志。
- 如果接收到了数据,会记录接收到的数据字节数和数据内容到日志,并通过
send()
函数将接收到的数据回传给客户端。由于send()
函数可能一次未能发送完所有数据,所以在一个循环中,将剩余的数据继续发送,直到所有数据都被发送出去。 - 如果在发送过程中出现了发送错误(
send()
返回值小于 0),则会记录错误信息到日志,并返回函数,放弃继续回传数据。 - 整个函数在循环中进行,直到
recv()
返回值小于等于 0,表示接收到的数据长度为 0(连接关闭)或出现了接收错误。
2.2.6 关闭连接和销毁套接字
shutdown();
:这个函数调用会关闭套接字的一部分或者全部通信。第二个参数指定了关闭方式:- 如果为 0,则表示关闭套接字的读取功能,即不能再从套接字中读取数据。
- 如果为 1,则表示关闭套接字的写入功能,即不能再向套接字中写入数据。
- 如果为 2,则表示关闭套接字的读取和写入功能,即完全关闭套接字的通信功能。
close(sock);
:这个函数调用会彻底关闭套接字,释放它占用的资源。关闭套接字后,不能再对它进行任何操作。
以上就是基本的TCP服务的编程流程,关于服务器实例请参考第三部分
三、示例
3.1 TCP客户端程序
代码见: https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_tcp_client
1 |
|
程序效果如下,可以正常收发数据:
3.2 TCP服务端程序
代码见: https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_tcp_server
1 |
|
效果演示:
值得注意的一点:这里将整个tcpserver的流程放在一个task里,以至于他只能一对一通信,若要连接多个,则需要将连接,接收的部分也作为task来编写。
每次建立连接就会创建一个新的套接字,将这个新的套接字放到一个新的线程进行通信,就能实现多个客户端连接。