ESP32WIFI-2.TCP协议

本文最后更新于:3 个月前

说明:

  1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。
  2. 本文档主要介绍ESP32的TCP协议,本节内容实现TCP客户端和服务端。
  3. 此文档仅作为个人学习笔记,本人不能保证其绝对准确性。

修订历史:

文档名称 版本 作者 时间 备注
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define TCP_SREVER_ADDR "192.168.1.100"
#define TCP_SREVER_PORT 8080

// 设置服务器(IPV4)
struct sockaddr_in server_config;
server_config.sin_addr.s_addr = inet_addr(TCP_SREVER_ADDR);
server_config.sin_family = AF_INET;
server_config.sin_port = htons(TCP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端)

// 连接服务器
int err = connect(sock, (struct sockaddr *)&server_config, sizeof(server_config));
if (err != 0)
{
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
break;
}

2.1.4 发送消息

使用send(int s,const void *dataptr,size_t size,int flags)函数发送消息,参数为:

  • s:指定的套接字描述符,即要发送消息的目标套接字。
  • dataptr:指向要发送数据的指针,可以是任意类型的数据。
  • size:要发送的数据大小,以字节为单位。
  • flags:用于指定发送操作的附加选项,通常可以设为0。

例如:

1
2
3
4
5
6
7
8
9
// 发送数据
const char *data = "Hello World!";
int len = send(sock, data, strlen(data), 0);
if (len < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}

2.1.5 接收消息

使用recv(int s,void *mem,size_t len,int flags)接收数据,参数为:

  • s:指定要接收数据的套接字描述符。
  • mem:指向存放接收数据的缓冲区的指针。
  • len:表示接收缓冲区的长度。
  • flags:指定接收操作的附加选项,通常可以设置为 0。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

int len = recv(sock, rx_buffer, sizeof(rx_buffer), 0);
// Error occurred during receiving
if (len < 0)
{
ESP_LOGE(TAG, "recv failed: errno %d", errno);
return;
}
// Data received
else
{
ESP_LOGI(TAG, "Received %d bytes from %s:", len, TCP_SREVER_ADDR);
ESP_LOGI(TAG, "%.*s", len, rx_buffer);
}

完整程序请看下面第三部分:

2.1 TCP服务器

2.2.1 准备工作

初始化NVS、 连接WIFI

2.2.2 创建并配置socket

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
// 设置套接字属性
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ESP_LOGI(TAG, "Socket created");

这里使用了一个用于设置 socket 属性,用函数 setsockopt(),函数原形如下:

1
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • sockfd:指定要设置选项的套接字文件描述符。
  • level:指定选项的协议级别。常见的级别包括 SOL_SOCKET(通用套接字选项)和 IPPROTO_TCP(TCP 协议选项)等。
  • optname:指定要设置的选项名称,可以是下列之一或者协议特定的选项。常见的选项包括 SO_REUSEADDR(允许地址重用)、SO_KEEPALIVE(启用连接保活)、SO_RCVBUF(设置接收缓冲区大小)等。
  • optval:指向包含选项值的缓冲区的指针。
  • optlen:指定选项值的长度。

2.2.3 配置服务器信息

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 设置服务器(IPV4)
struct sockaddr_storage dest_addr;
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(TCP_SREVER_PORT);

// 绑定套接字
int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0)
{
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
ESP_LOGE(TAG, "IPPROTO: %d", AF_INET);
goto CLEAN_UP;
}
ESP_LOGI(TAG, "Socket bound, port %d", TCP_SREVER_PORT);

bind 函数用于将一个套接字与一个地址(通常是 IP 地址和端口号)绑定在一起,以便在该地址上监听连接或发送数据。它的原型如下:

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. sockfd:指定要绑定地址的套接字文件描述符。
  2. addr:指向一个 sockaddr 结构体的指针,该结构体包含了要绑定的地址信息。在 IPv4 地址族中,可以使用 sockaddr_in 结构体;在 IPv6 地址族中,可以使用 sockaddr_in6 结构体。通常,你需要将地址信息转换为 sockaddr 结构体的形式,然后传递给 bind 函数。
  3. addrlen:指定地址结构体的长度。

2.2.4 监听客户端连接

1
2
3
4
5
6
7
// 监听套接字
err = listen(listen_sock, 1);
if (err != 0)
{
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
goto CLEAN_UP;
}

listen() 函数用于将一个套接字(通常是服务器套接字)转换为被动套接字,即用于接受连接请求

  1. sockfd:指定要监听的套接字文件描述符。
  2. backlog:指定在内核中排队等待接受连接的最大连接数。这个参数限制了同时等待连接的数量,超过这个数量的连接请求将被拒绝。这并不是一个限制同时连接的数量,而是限制等待连接队列的长度。
  3. listen() 函数在成功时返回 0,失败时返回 -1

2.2.4 建立接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ESP_LOGI(TAG, "Socket listening");
struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6
socklen_t addr_len = sizeof(source_addr);
int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (sock < 0)
{
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}


// Set tcp keepalive option
int keepAlive = 1;
int keepIdle = 5; // TCP keep-alive idle time(s)
int keepInterval = 5; // TCP keep-alive interval time(s)
int keepCount = 3; // TCP keep-alive packet retry send counts

setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));


sockaddr_storage 是一个足够大的结构体,可用于存储任意地址族(IPv4 或 IPv6)的地址信息。

建立连接使用函数accept(),它用于接受传入的连接请求,并创建一个新的套接字来与客户端进行通信。它的原型如下:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • 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 接收/发送数据

接收和发送依然使用recvsend,下面实现了一个简单的数据接收,回传函数,参数为建立连接的套接字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 数据接收与回传
static void do_retransmit(const int sock)
{
int len;
char rx_buffer[128];

do
{
len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0)
{
ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno);
}
else if (len == 0)
{
ESP_LOGW(TAG, "Connection closed");
}
else
{
rx_buffer[len] = 0; // Null-terminate whatever is received and treat it like a string
ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer);

// send() can return less bytes than supplied length.
// Walk-around for robust implementation.
int to_write = len;
while (to_write > 0)
{
int written = send(sock, rx_buffer + (len - to_write), to_write, 0);
if (written < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
// Failed to retransmit, giving up
return;
}
to_write -= written;
}
}
} while (len > 0);
}

程序解释如下:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include <sys/socket.h>

#define ESP_WIFI_STA_SSID "duruofu_win10"
#define ESP_WIFI_STA_PASSWD "1234567890"
#define TCP_SREVER_ADDR "192.168.137.1"
#define TCP_SREVER_PORT 8080

static const char *TAG = "main";

void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
static uint8_t connect_count = 0;
// WIFI 启动成功
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START");
ESP_ERROR_CHECK(esp_wifi_connect());
}
// WIFI 连接失败
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED");
connect_count++;
if (connect_count < 6)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_ERROR_CHECK(esp_wifi_connect());
}
else
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times");
}
}
// WIFI 连接成功(获取到了IP)
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP");
ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data;
ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip));
}
}

static void tcp_client_task(void *pvParameters)
{


// 等待wifi连接成功(暂时这样处理)
vTaskDelay(5000 / portTICK_PERIOD_MS);

ESP_LOGI("tcp_client_task", "tcp_client_task start");

// 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) // 创建失败返回-1
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
return;
}

// 设置服务器(IPV4)
struct sockaddr_in server_config;
server_config.sin_addr.s_addr = inet_addr(TCP_SREVER_ADDR);
server_config.sin_family = AF_INET;
server_config.sin_port = htons(TCP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端)

// 连接服务器
int err = connect(sock, (struct sockaddr *)&server_config, sizeof(server_config));
if (err != 0)
{
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
return;
}

// 发送数据
const char *data = "Hello World!";
int len = send(sock, data, strlen(data), 0);
if (len < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
return;
}

char rx_buffer[1024];
// 接收数据,并发回
while(1)
{
int len = recv(sock, rx_buffer, sizeof(rx_buffer), 0);
// Error occurred during receiving
if (len < 0)
{
ESP_LOGE(TAG, "recv failed: errno %d", errno);
break;
}
// Data received
else
{
ESP_LOGI(TAG, "Received %d bytes from %s:", len, TCP_SREVER_ADDR);
ESP_LOGI(TAG, "%.*s", len, rx_buffer);

// 发送数据
int len_end = send(sock, rx_buffer, len, 0);
if (len_end < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
}
}

vTaskDelete(NULL);
}

// wifi初始化
static void wifi_sta_init(void)
{
ESP_ERROR_CHECK(esp_netif_init());

// 注册事件(wifi启动成功)
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL));
// 注册事件(wifi连接失败)
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL));
// 注册事件(wifi连接失败)
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL));

// 初始化STA设备
esp_netif_create_default_wifi_sta();

/*Initialize WiFi */
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏

ESP_ERROR_CHECK(esp_wifi_init(&cfg));

//----------------配置阶段-------------------
// 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API)
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

// STA详细配置
wifi_config_t sta_config = {
.sta = {
.ssid = ESP_WIFI_STA_SSID,
.password = ESP_WIFI_STA_PASSWD,
.bssid_set = false,
},
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));

//----------------启动阶段-------------------
ESP_ERROR_CHECK(esp_wifi_start());

//----------------配置省电模式-------------------
// 不省电(数据传输会更快)
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
}

void app_main(void)
{
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

// 创建默认事件循环
ESP_ERROR_CHECK(esp_event_loop_create_default());

// 配置启动WIFI
wifi_sta_init();

// 创建TCP客户端任务
xTaskCreate(tcp_client_task, "tcp_client_task", 4096, NULL, 5, NULL);
}

程序效果如下,可以正常收发数据:

3.2 TCP服务端程序

代码见: https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_tcp_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include <sys/socket.h>

#define ESP_WIFI_STA_SSID "duruofu_win10"
#define ESP_WIFI_STA_PASSWD "1234567890"

#define TCP_SREVER_PORT 8080

static const char *TAG = "main";

void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
static uint8_t connect_count = 0;
// WIFI 启动成功
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START");
ESP_ERROR_CHECK(esp_wifi_connect());
}
// WIFI 连接失败
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED");
connect_count++;
if (connect_count < 6)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_ERROR_CHECK(esp_wifi_connect());
}
else
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times");
}
}
// WIFI 连接成功(获取到了IP)
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP");
ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data;
ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip));
}
}

// 数据接收与回传
static void do_retransmit(const int sock)
{
int len;
char rx_buffer[128];

do
{
len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0)
{
ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno);
}
else if (len == 0)
{
ESP_LOGW(TAG, "Connection closed");
}
else
{
rx_buffer[len] = 0; // Null-terminate whatever is received and treat it like a string
ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer);

// send() can return less bytes than supplied length.
// Walk-around for robust implementation.
int to_write = len;
while (to_write > 0)
{
int written = send(sock, rx_buffer + (len - to_write), to_write, 0);
if (written < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
// Failed to retransmit, giving up
return;
}
to_write -= written;
}
}
} while (len > 0);
}

// tcp服务器任务
static void tcp_server_task(void *pvParameters)
{
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
// 设置套接字属性
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ESP_LOGI(TAG, "Socket created");

// 设置服务器(IPV4)
struct sockaddr_storage dest_addr;
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(TCP_SREVER_PORT);

// 绑定套接字
int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0)
{
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
ESP_LOGE(TAG, "IPPROTO: %d", AF_INET);
goto CLEAN_UP;
}
ESP_LOGI(TAG, "Socket bound, port %d", TCP_SREVER_PORT);

// 监听套接字 (阻塞)
err = listen(listen_sock, 1);
if (err != 0)
{
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
goto CLEAN_UP;
}

while (1)
{
char addr_str[128];

ESP_LOGI(TAG, "Socket listening");
struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6
socklen_t addr_len = sizeof(source_addr);
int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (sock < 0)
{
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}

// Set tcp keepalive option
int keepAlive = 1;
int keepIdle = 5; // TCP keep-alive idle time(s)
int keepInterval = 5; // TCP keep-alive interval time(s)
int keepCount = 3; // TCP keep-alive packet retry send counts

setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));

// Convert ip address to string
if (source_addr.ss_family == PF_INET)
{
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}

ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);

do_retransmit(sock);

shutdown(sock, 0);
close(sock);
}

CLEAN_UP:
close(listen_sock);
vTaskDelete(NULL);
}

// wifi初始化
static void wifi_sta_init(void)
{
ESP_ERROR_CHECK(esp_netif_init());

// 注册事件(wifi启动成功)
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL));
// 注册事件(wifi连接失败)
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL));
// 注册事件(wifi连接失败)
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL));

// 初始化STA设备
esp_netif_create_default_wifi_sta();

/*Initialize WiFi */
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏

ESP_ERROR_CHECK(esp_wifi_init(&cfg));

//----------------配置阶段-------------------
// 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API)
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

// STA详细配置
wifi_config_t sta_config = {
.sta = {
.ssid = ESP_WIFI_STA_SSID,
.password = ESP_WIFI_STA_PASSWD,
.bssid_set = false,
},
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));

//----------------启动阶段-------------------
ESP_ERROR_CHECK(esp_wifi_start());

//----------------配置省电模式-------------------
// 不省电(数据传输会更快)
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
}

void app_main(void)
{
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

// 创建默认事件循环
ESP_ERROR_CHECK(esp_event_loop_create_default());

// 配置启动WIFI
wifi_sta_init();

// 创建TCP服务器任务
xTaskCreate(tcp_server_task, "tcp_server_task", 4096, NULL, 5, NULL);
}

效果演示:

值得注意的一点:这里将整个tcpserver的流程放在一个task里,以至于他只能一对一通信,若要连接多个,则需要将连接,接收的部分也作为task来编写。
每次建立连接就会创建一个新的套接字,将这个新的套接字放到一个新的线程进行通信,就能实现多个客户端连接。

参考链接

  1. https://github.com/espressif/esp-idf/blob/release/v5.1/examples/protocols/sockets/tcp_server/main/tcp_server.c

ESP32WIFI-2.TCP协议
https://www.duruofu.xyz/2024/03/15/4.硬件相关/MCU/ESP32/05.ESP32WIFI入门/5.2-ESP32网络入门-TCP协议/ESP32网络入门-TCP协议/
作者
DuRuofu
发布于
2024年3月15日
更新于
2024年3月15日
许可协议