关注嘉友创科技公众号

源码地址:https://github.com/HX-IoT/ESP32-Developer-Guide
ESP32开发指南QQ群:824870185,内有pdf版,排版整洁。
学习目的及目标
掌握TCP原理和工作过程
掌握乐鑫ESP32的TCP的程序设计
主要掌握TCP作为Client的详细过程
TCP科普(来自百度百科)
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内, 另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元( MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
连接终止
终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。 [1]
某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN
TCP特点和流程
上面的原理很重要,但毕竟我们只是在API之上做应用。只需要了解特点和流程。知道特点可以做方案时候考量可行性,流程就是可行后的实施。
面向连接的:发数据前要进行连接。
可靠的连接:TCP连接传送的数据,无差错,不丢失,不重复,且按序到达。
点到点:TCP连接传送的数据,无差错,不丢失,不重复,且按序到达
最大长度有限:仅1500字节。(http和websocket有了用武之地)
TCP编程的客户端一般步骤是:
创建一个socket,用函数socket();
设置socket属性,用函数setsockopt();(可选)
绑定IP地址、端口等信息到socket上,用函数bind();* 可选
设置要连接的对方的IP地址和端口等属性;
连接服务器,用函数connect();
收发数据,用函数send()和recv(),或者read()和write();
关闭网络连接;
TCP编程的服务器端一般步骤是:
创建一个socket,用函数socket();
设置socket属性,用函数setsockopt();(可选)
绑定IP地址、端口等信息到socket上,用函数bind();
开启监听,用函数listen();
接收客户端上来的连接,用函数accept();
关闭网络连接; closesocket();
关闭监听;
软件设计
ESP32使用的是LwIP,LwIP是特别适用于嵌入式设备的小型开源TCP/IP协议栈,对内存资源占用很小。ESP-IDF即是移植了LwIP协议栈。学习了解LwIP,给大家推荐本书,《嵌入式网络那些事:LwIP协议深度剖析与实战演练》。
我们的这个例程是直接怼的是标准socket接口(内部是LwIP封装的),没有用LwIP的,关于LwIP的接口讲解在Websocket中讲解,用法都是一样,知道流程后,API调用即可,处理好异常。流程+接口,打遍无敌手。LwIP的教程可以参考安富莱、野火的文档。
在src/include/lwip/socket.h文件中可以看到下面的宏定义,lwip的socket也提供标准的socket接口函数。
<col>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#if LWIP_COMPAT_SOCKETS
#define accept(a,b,c) lwip_accept(a,b,c)
#define bind(a,b,c) lwip_bind(a,b,c)
#define shutdown(a,b) lwip_shutdown(a,b)
#define closesocket(s) lwip_close(s)
#define connect(a,b,c) lwip_connect(a,b,c)
#define getsockname(a,b,c) lwip_getsockname(a,b,c)
#define getpeername(a,b,c) lwip_getpeername(a,b,c)
#define setsockopt(a,b,c,d,e) lwip_setsockopt(a,b,c,d,e)
#define getsockopt(a,b,c,d,e) lwip_getsockopt(a,b,c,d,e)
#define listen(a,b) lwip_listen(a,b)
#define recv(a,b,c,d) lwip_recv(a,b,c,d)
#define recvfrom(a,b,c,d,e,f) lwip_recvfrom(a,b,c,d,e,f)
#define send(a,b,c,d) lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f) lwip_sendto(a,b,c,d,e,f)
#define socket(a,b,c) lwip_socket(a,b,c)
#define select(a,b,c,d,e) lwip_select(a,b,c,d,e)
#define ioctlsocket(a,b,c) lwip_ioctl(a,b,c)
#if LWIP_POSIX_SOCKETS_IO_NAMES
#define read(a,b,c) lwip_read(a,b,c)
#define write(a,b,c) lwip_write(a,b,c)
#define close(s) lwip_close(s)
新建socket函数:socket();
连接函数:connect();
关闭socket函数:close();
获取socket错误代码:getsocketopt();
接收数据函数:recv();
发送数据函数:send();
绑定函数:bing();
监听函数:listen();
获取连接函数:accept();
更多更详细接口请参考官方指南。
ESP32的TCP总结
初始化wifi配置后,程序会根据WIFI的实时状态,在回调函数中给出状态返回,所以只需要在回调中进行相关操作,STA开始事件触发TCP进行连接,连接上后就可以进行数据的交互。其中对连接的异常情况做出来显得异常重要,TCP是可靠的,不能玩成地摊货。
只讲Client,server看源码。
24
25
26
27
28
29
30
31
32
esp_err_t create_tcp_client()
{
ESP_LOGI(TAG, "will connect gateway ssid : %s port:%d",
TCP_SERVER_ADRESS, TCP_PORT);
//新建socket
connect_socket = socket(AF_INET, SOCK_STREAM, 0);
if (connect_socket < 0)
{
//打印报错信息
show_socket_error_reason("create client", connect_socket);
//新建失败后,关闭新建的socket,等待下次新建
close(connect_socket);
return ESP_FAIL;
}
//配置连接服务器信息:端口+ip
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(TCP_PORT);
server_addr.sin_addr.s_addr = inet_addr(TCP_SERVER_ADRESS);
ESP_LOGI(TAG, "connectting server...");
//连接服务器
if (connect(connect_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
show_socket_error_reason("client connect", connect_socket);
ESP_LOGE(TAG, "connect failed!");
//连接失败后,关闭之前新建的socket,等待下次新建
ESP_LOGI(TAG, "connect success!");
return ESP_OK;
}
33
34
void recv_data(void *pvParameters)
int len = 0; //长度
char databuff[1024]; //缓存
while (1)
//清空缓存
memset(databuff, 0x00, sizeof(databuff));
//读取接收数据
len = recv(connect_socket, databuff, sizeof(databuff), 0);
//异常标记
g_rxtx_need_restart = false;
if (len > 0)
{
//打印接收到的数组
ESP_LOGI(TAG, "recvData: %s", databuff);
//接收数据回发
send(connect_socket, databuff, strlen(databuff), 0);
}
else
//打印错误信息
show_socket_error_reason("recv_data", connect_socket);
//服务器故障,标记重连
g_rxtx_need_restart = true;
break;
close_socket();
//标记重连
g_rxtx_need_restart = true;
vTaskDelete(NULL);
static void tcp_connect(void *pvParameters)
。。。。。。。
while (1)
vTaskDelay(3000 / portTICK_RATE_MS);
//重新建立client,和新建一样一样
if (g_rxtx_need_restart)
{
vTaskDelay(3000 / portTICK_RATE_MS);
ESP_LOGI(TAG, "reStart create tcp client...");
//建立client
int socket_ret = create_tcp_client();
if (socket_ret == ESP_FAIL)
{
ESP_LOGE(TAG, "reStart create tcp socket error,stop...");
continue;
}
else
ESP_LOGI(TAG, "reStart create tcp socket succeed...");
//重新建立完成,清除标记
g_rxtx_need_restart = false;
//建立tcp接收数据任务
xTaskCreate(&recv_data, "recv_data", 4096, NULL, 4, &tx_rx_task);
}
测试流程和效果展示
Client测试
修改AP和STA的账号密码
#define TCP_SERVER_CLIENT_OPTION FALSE //esp32作为client
修改作为client连接server的IP(电脑/手机)和Port
使用手机或者电脑使用助手工具建立server,让esp32自动连接
Server测试
#define TCP_SERVER_CLIENT_OPTION TRUE //esp32作为server
修改作为Server时监听的Port
手机或者电脑直连ESP32的AP
使用TCP助手工具作为Client,连接esp32的server
效果展示
Client效果展示
先建服务器,等ESP32过来连接。
测试发送数据
压力小测
Server效果展示
连接ESP32的AP
测试异常
TCP总结
底层重原理,应用重流程+接口。
压力小测不丢包,自己移植要大测产品稳定性。
源码地址:https://github.com/xiaolongba/wireless-tech
<b> </b>