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三次握手的过程如下:
终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。 [1]
上面的原理很重要,但毕竟我们只是在API之上做应用。只需要了解特点和流程。知道特点可以做方案时候考量可行性,流程就是可行后的实施。
TCP编程的客户端一般步骤是:
TCP编程的服务器端一般步骤是:
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接口函数。
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) |
更多更详细接口请参考官方指南。
初始化wifi配置后,程序会根据WIFI的实时状态,在回调函数中给出状态返回,所以只需要在回调中进行相关操作,STA开始事件触发TCP进行连接,连接上后就可以进行数据的交互。其中对连接的异常情况做出来显得异常重要,TCP是可靠的,不能玩成地摊货。
只讲Client,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 |
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,等待下次新建 close(connect_socket); return ESP_FAIL; } ESP_LOGI(TAG, "connect success!"); return ESP_OK; } |
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 |
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); } |
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 |
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); } } } vTaskDelete(NULL); } return ESP_OK; } |
先建服务器,等ESP32过来连接。
测试发送数据
压力小测
连接ESP32的AP
测试发送数据
压力小测
测试异常
点我->更多ESP32开发指南系列目录