第十六章 ESP32的TCP连接

源码地址:https://github.com/xiaolongba/wireless-tech

ESP32开发指南QQ群:824870185,内有pdf版,排版整洁。

第十六章 ESP32的TCP连接

学习目的及目标

  • 掌握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状态。
      1. 连接终止

终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。 [1] 

  • 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  • 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
  • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  • 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN

TCP特点和流程

上面的原理很重要,但毕竟我们只是在API之上做应用。只需要了解特点流程。知道特点可以做方案时候考量可行性,流程就是可行后的实施。

TCP特点:

  • 面向连接的:发数据前要进行连接。
  • 可靠的连接:TCP连接传送的数据,无差错,不丢失,不重复,且按序到达。
  • 点到点:TCP连接传送的数据,无差错,不丢失,不重复,且按序到达
  • 最大长度有限:仅1500字节。(http和websocket有了用武之地

TCP流程: (本段来源

TCP编程的客户端一般步骤是:

  • 创建一个socket,用函数socket(); 
  • 设置socket属性,用函数setsockopt();(可选)
  • 绑定IP地址、端口等信息到socket上,用函数bind();* 可选 
  • 设置要连接的对方的IP地址和端口等属性; 
  • 连接服务器,用函数connect(); 
  • 收发数据,用函数send()和recv(),或者read()和write(); 
  • 关闭网络连接;

TCP编程的服务器端一般步骤是: 

  1. 创建一个socket,用函数socket();
  2. 设置socket属性,用函数setsockopt();(可选) 
  3. 绑定IP地址、端口等信息到socket上,用函数bind();
  4. 开启监听,用函数listen();
  5. 接收客户端上来的连接,用函数accept();
  6. 收发数据,用函数send()和recv(),或者read()和write(); 
  7. 关闭网络连接; closesocket();
  8. 关闭监听; 

TCP和UDP(下一章讲)互怼

第十六章 ESP32的TCP连接

 

软件设计

ESP32的TCP Client(Server类似)主逻辑

第十六章 ESP32的TCP连接

 

TCP Client的新建任务和接收任务详细过程逻辑

第十六章 ESP32的TCP连接

 

 

ESP32的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)

  • 新建socket函数:socket();
  • 连接函数:connect();
  • 关闭socket函数:close();
  • 获取socket错误代码:getsocketopt();
  • 接收数据函数:recv();
  • 发送数据函数:send();
  • 绑定函数:bing();
  • 监听函数:listen();
  • 获取连接函数:accept();

更多更详细接口请参考官方指南

​​​​​​​​​​​​​​

ESP32的TCP总结

初始化wifi配置后,程序会根据WIFI的实时状态,在回调函数中给出状态返回,所以只需要在回调中进行相关操作,STA开始事件触发TCP进行连接,连接上后就可以进行数据的交互。其中对连接的异常情况做出来显得异常重要,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;

}

 

 

TCP接收任务代码

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);

}

 

 

TCP异常处理

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;

}

 

测试流程和效果展示

测试流程

  • Client测试
  1. 修改AP和STA的账号密码
  2. #define TCP_SERVER_CLIENT_OPTION FALSE //esp32作为client
  3. 修改作为client连接server的IP(电脑/手机)和Port
  4. 使用手机或者电脑使用助手工具建立server,让esp32自动连接
  • Server测试
  1. #define TCP_SERVER_CLIENT_OPTION TRUE //esp32作为server
  2. 修改作为Server时监听的Port
  3. 手机或者电脑直连ESP32的AP
  4. 使用TCP助手工具作为Client,连接esp32的server
      1. 效果展示

Client效果展示

先建服务器,等ESP32过来连接。

第十六章 ESP32的TCP连接

测试发送数据

第十六章 ESP32的TCP连接

压力小测

第十六章 ESP32的TCP连接

Server效果展示

连接ESP32的AP

第十六章 ESP32的TCP连接

测试发送数据

第十六章 ESP32的TCP连接

压力小测

第十六章 ESP32的TCP连接

测试异常

第十六章 ESP32的TCP连接

 

TCP总结