【ESP32】ESP-IDF开发 | WiFi开发 | TCP传输控制协议 + TCP服务器和客户端例程

1. 简介

TCP(Transmission Control Protocol),全称传输控制协议 。它的特点有以下几点:面向连接 ,每一个TCP连接只能是点对点的(一对一);提供可靠交付服务 ;提供全双工通信面向字节流

1.1 三次握手

三次握手代表的是TCP的连接过程,它表示在成功连接之前,服务端和客户端需要通信三次才能最终确认。

  • 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认;
  • 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态;
  • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。

1.2 四次挥手

四次挥手代表的是TCP的断开连接过程,它表示在成功断开前,服务端和客户端需要通信四次才能最终确认。

  • 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态,表示客户端没有数据需要发送了;但是如果服务器端还有数据没有发送完成,则可以继续发送数据;
  • 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端请求收到了,但是我还没准备好,要继续等待我的消息;这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文;
  • 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端数据发完了,准备关闭连接;服务器端进入LAST_ACK状态;
  • 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后会进入TIME_WAIT状态,如果服务端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。如果客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那客户端也可以关闭连接了。

1.3 拥塞控制

网络就像我们生活中的交通系统,当车流大的时候就可能会导致拥塞,TCP为了保证可靠的交付服务,所以引入了拥塞控制,灵活调整发送策略。

拥塞控制是一个动态的过程,它既要提高带宽利用率发送尽量多的数据又要避免网络拥堵丢包RTT增大等问题,基于这种高要求并不是单一策略可以搞定的,因此TCP的拥塞控制策略实际上是分阶段分策略的综合过程,包括慢开始(slow start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery)。

1. 慢开始

慢开始算法的思路为,在数据开始发送时,由于不清楚网络的负荷情况,如果此时立即把大量数据发送到网络,那么就有可能引起网络拥塞。根据生活中的经验进行引伸,较好的方法是由小大到逐渐增大发送窗口,一步步探测网络链路的极限;也就是说,由小到大逐渐增大拥塞窗口数值(cwnd),下面是其简要工作流程图。

由上图可见,一开始发送方的初始cwnd 为1,发送方发送第一个报文段M1,并收到接收方的确认。此时,发送方将cwnd从1增大到2,接着发送M2和M3两个报文段,收到两个报文的确认后,发送方继续将cwnd加倍,增加到4。只要网络仍然通畅,那么发送方就会以此类推,在每一轮的传输成功后将cwnd进行加倍的操作。

显然,cwnd不能无限制地加倍,这样会引起网络拥塞,因此需要设置一个慢开始门限(ssthresh)状态变量。当cwnd < ssthresh时,才会使用上述的慢开始算法。

2. 拥塞避免

当cwnd > ssthresh时,系统会使用拥塞避免算法,该算法的思路是让拥塞窗口(cwnd)缓慢地增长,即每完成一轮传输就把发送方的拥塞窗口(cwnd)加1,而不是像慢开始阶段那样加倍增长。因此在拥塞避免阶段就有"加法增大"(Additive Increase)的特点。这表明在拥塞避免阶段,拥塞窗口(cwnd)按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。下图展示了拥塞控制的工作流程。

在上图中,慢开始门限的初始值为16。一开始系统执行慢开始算法,发送方每成功发送一轮报文段,就把拥塞窗口值加倍,然后开始下一轮的传输。因此拥塞窗口cwnd随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢开始门限值ssthresh 时,就开始改为执行拥塞避免算法,拥塞窗口按线性规律增长。

当进行到第12轮传输时(上图节点2),网络出现了超时,发送方判断为网络拥塞。于是调整门限值ssthresh = cwnd / 2 = 12,同时设置拥塞窗口cwnd = 1,重新进入慢开始阶段。

上图节点3展示了一个特殊情况,此时拥塞窗口cwnd = 16,这时出现了发送方一连收到3个对同一报文段的重复确认的情况,此时执行了拥塞避免算法,调整门限值ssthresh = cwnd / 2 = 8。这是因为如果发送方迟迟收不到确认,就会产生超时,会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又置为1,因而降低了传输效率。

3. 快重传

TCP作为一个可靠的协议面临的很大的问题就是丢包,丢包就要重传因此发送方需要根据接收方回复的ACK来确认是否丢包了,下图为超时重传的典型时序图。

重传超时时间(RTO)是随着复杂网络环境而动态变化的,在拥塞控制中发生超时重传将会极大拉低cwnd,如果网络状况并没有那么多糟糕,偶尔出现网络抖动造成丢包或者阻塞也非常常见,因此触发的慢启动将降低通信性能,故出现了快速重传机制。所谓快速重传时相比超时重传而言的,重发等待时间会降低并且后续尽量避免慢启动,来保证性能损失在最小的程度,下图为其时序图。

快速重传和超时重传的区别在于cwnd在发生拥塞时的取值,超时重传会将cwnd修改为最初的值,也就是慢启动的值,快速重传将cwnd减半,二者都将ssthresh设置为cwnd的一半。从二者的区别可以看到,快速重传更加主动,有利于保证链路的传输性能。

4. 快恢复

在快速重传之后就会进入快速恢复阶段,此时的cwnd为上次发生拥塞时的cwnd的1/2,之后cwnd再线性增加重复之前的过程。

2. lwIP

ESP-IDF使用lwIP库实现TCP/IP协议栈,这个库在大多数嵌入式系统中都有用到,它是对底层硬件的上层封装,所以如果未来要写比如Linux的TCP/IP应用,代码也是通用的。

3. 例程

例程分别在ESP32上实现TCP客户端和服务端,使用电脑作为另一方进行简单通信测试。需要注意的是,测试时,ESP32和电脑必须处于同一局域网。

电脑端测试会使用的上位机为野火串口调试助手,下载地址:FireTools

3.1 客户端

这个例程配置ESP32为客户端,当连接WiFi热点成功后会请求连接服务端,连接成功后会发送一段消息,然后阻塞等待服务端回复,服务端恢复消息后ESP32会主动关闭套接字。

cpp 复制代码
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"

#include <string.h>

#define TAG "app"
#define HOST_IP_ADDR "192.168.10.117"
#define HOST_PORT 20001


static char rx_buffer[128];
static const char *payload = "Message from ESP32";
static TaskHandle_t client_task_handle;

static void tcp_client_task(void *args)
{
    struct sockaddr_in dest_addr;
    inet_pton(AF_INET, HOST_IP_ADDR, &dest_addr.sin_addr);
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(HOST_PORT);

    while (1) {
        int sock =  socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
        if (sock < 0) {
            ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket created, connecting to %s:%d", HOST_IP_ADDR, HOST_PORT);

        int err = connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (err != 0) {
            ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Successfully connected");

        err = send(sock, payload, strlen(payload), 0);
        if (err < 0) {
            ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
            break;
        }

        memset(rx_buffer, 0, sizeof(rx_buffer));
        int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
        if (len < 0) {
            ESP_LOGE(TAG, "recv failed: errno %d", errno);
        } else {
            ESP_LOGI(TAG, "Received %d bytes from %s:", len, HOST_IP_ADDR);
            ESP_LOGI(TAG, "data: %s", rx_buffer);
        }

        close(sock);

        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

static void wifi_event_handler(void* arg,
                               esp_event_base_t event_base,
                               int32_t event_id,
                               void* event_data)
{
    if (event_base == IP_EVENT) {
        if (event_id == IP_EVENT_STA_GOT_IP) {
            xTaskCreate(tcp_client_task, "tcp_client", 2048, NULL, 5, &client_task_handle);
        }
    } else if (event_base == WIFI_EVENT) {
        if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
            vTaskDelete(client_task_handle);
        } else if (event_id == WIFI_EVENT_STA_START) {
            esp_wifi_connect();
        }
    }
}

int app_main()
{
    /* 初始化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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    /* 初始化WiFi协议栈 */
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "Your SSID",
            .password = "Your password",
            .threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
        },
    };

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    return 0;
}

ESP32的WiFi驱动初始化在前面的文章已经有详细的介绍了,这里不再赘述。

在回调函数中,当驱动获取到IP后,就会创建TCP客户端的任务。

1. 创建socket套接字

调用socket函数创建,第一个参数表示域,这里使用IPv4,对应IP_INET;第二个参数表示socket类型,TCP协议只能填SOCK_STREAM;第三个参数表示协议栈类型,这里填IPPROTO_IP。函数会返回套接字描述符。

2. 连接服务器

调用connect函数,第一个参数传入套接字描述符;比较重要的是第二个参数,要传入服务器的地址信息。结构体的定义如下:

cpp 复制代码
struct sockaddr_in {
  u8_t            sin_len;
  sa_family_t     sin_family;
  in_port_t       sin_port;
  struct in_addr  sin_addr;
#define SIN_ZERO_LEN 8
  char            sin_zero[SIN_ZERO_LEN];
};
  • sin_len:数据长度(一般不需要填);
  • sin_family:套接字类型,IPv4填AF_INET,IPv6填AF_INET6,其他填AF_UNSPEC;
  • sin_port:端口;
  • sin_zero:上层预留字节(不用管)。

3. 发送数据

调用send函数。传入套接字描述符、数据指针和数据长度即可;最后一个参数是标志位,一般填0即可,可选的标志位如下:

cpp 复制代码
#define MSG_PEEK       0x01
#define MSG_WAITALL    0x02
#define MSG_OOB        0x04
#define MSG_DONTWAIT   0x08
#define MSG_MORE       0x10
#define MSG_NOSIGNAL   0x20

这些标志位是发送和接收都支持的,比较常用的是MSG_DONTWAIT,像发送和接收函数是阻塞的,使能这个标志位可以让函数立即返回,不等待数据。

4. 接收数据

调用recv函数。传入的参数与send函数是一致的,不再赘述。

5. 关闭连接

调用close函数。传入套接字描述符即可。

测试的时候先打开上位机,设置为TCP服务器,填写电脑的IP和端口,端口是自定义的,但注意不要与原有的端口冲突,建议设置20000以上比较保险;最后点击开始监听。

3.2 服务端

这个例程就是在ESP32上搭建一个TCP服务器,接受局域网中的客户端连接并接收数据。

cpp 复制代码
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"

#include <string.h>

#define TAG "app"
#define HOST_PORT 20001

static const char *payload = "I have received your message";
static TaskHandle_t server_task_handle;


static void tcp_client_task(void *args)
{
    int *sock = args;
    int len;
    char rx_buffer[128] = {0};

    while (1) {
        memset(rx_buffer, 0, sizeof(rx_buffer));
        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");
            goto __exit;
        } else {
            ESP_LOGI(TAG, "Received %d bytes, data: %s", len, rx_buffer);
            send(*sock, payload, strlen(payload), 0);
        }
    }

__exit:
    close(*sock);
    free(sock);
    vTaskDelete(NULL);
}

static void tcp_server_task(void *args)
{
    esp_ip4_addr_t *ip_addr = args;

    struct sockaddr_in dest_addr = {0};
    dest_addr.sin_addr.s_addr = ip_addr->addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(HOST_PORT);

    int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (sock < 0) {
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
        goto __exit;
    }

    int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if (err != 0) {
        ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
        goto __exit;
    }

    err = listen(sock, 1);
    if (err != 0) {
        ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
        goto __exit;
    }

    ESP_LOGI(TAG, "Server listen at " IPSTR ":%d", IP2STR(ip_addr), HOST_PORT);

    while (1) {
        struct sockaddr_in source_addr = {0};
        socklen_t addr_len = sizeof(struct sockaddr_in);
        int *client = malloc(sizeof(int));
        *client = accept(sock, (struct sockaddr *)&source_addr, &addr_len);
        if (*client < 0) {
            ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
        } else {
            ESP_LOGI(TAG, "Client " IPSTR ":%d connected", IP2STR((struct esp_ip4_addr *)&source_addr.sin_addr), source_addr.sin_port);
            xTaskCreate(tcp_client_task, "client_task", 2048, client, 6, NULL);
        }
    }

__exit:
    close(sock);
    free(ip_addr);
    vTaskDelete(NULL);
}

static void wifi_event_handler(void* arg,
                               esp_event_base_t event_base,
                               int32_t event_id,
                               void* event_data)
{
    if (event_base == IP_EVENT) {
        if (event_id == IP_EVENT_STA_GOT_IP) {
            ip_event_got_ip_t *data = event_data;
            esp_ip4_addr_t *ip_addr = malloc(sizeof(esp_ip4_addr_t));
            memcpy(ip_addr, &data->ip_info.ip, sizeof(esp_ip4_addr_t));
            xTaskCreate(tcp_server_task, "tcp_server", 2048, ip_addr, 5, &server_task_handle);
        }
    } else if (event_base == WIFI_EVENT) {
        if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
            vTaskDelete(server_task_handle);
        } else if (event_id == WIFI_EVENT_STA_START) {
            esp_wifi_connect();
        }
    }
}

int app_main()
{
    /* 初始化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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    /* 初始化WiFi协议栈 */
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "Your SSID",
            .password = "Your password",
            .threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
        },
    };

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    return 0;
}

服务器的代码与客户端是有一部分重合的,所以下面只重点介绍不同的地方。

1. 创建套接字

参考上面。

2. 绑定IP与端口

调用bind函数。传入的参数其实跟connect函数是一样的;但是这里的IP地址是自己的IP地址,端口的话就是自定义的。

3. 监听端口

调用listen函数。第一个参数传入套接字描述符,第二个参数用来使能log记录。

4. 接受客户端连接

调用accept函数。传入套接字描述符、IP地址结构体和结构体的长度。这个结构体是用来接收客户端的IP信息的,初始化为空即可。这个函数是阻塞的,只要没有客户端连接就不会返回;如果有客户端连接,就会返回该客户端的套接字描述符。

例程中,一旦客户端成功连接就会创建一个线程处理这个客户端的数据,这样的话就能实现多客户端的连接服务。

5. 接收数据

参考上面。如果recv函数返回0则代表客户端断开了连接,这时我们就可以关闭这个套接字,退出线程。

6. 发送数据

参考上面。

测试时先将ESP32上电,确保服务器已经启动并处于监听状态;然后在上位机这里设置为TCP客户端模式,填入ESP32的IP和端口;然后就可以连接并发送数据了。

相关推荐
漫无目的行走的月亮13 分钟前
51单片机开发:矩阵按键实验
单片机·嵌入式硬件·51单片机
Icomi_1 小时前
【外文原版书阅读】《机器学习前置知识》1.线性代数的重要性,初识向量以及向量加法
c语言·c++·人工智能·深度学习·神经网络·机器学习·计算机视觉
apocelipes1 小时前
Linux glibc自带哈希表的用例及性能测试
c语言·c++·哈希表·linux编程
仓鼠OO1 小时前
思科交换机telnet配置案例
网络·思科·远程
Tanecious.1 小时前
C语言--分支循环实践:猜数字游戏
android·c语言·游戏
Andya_net1 小时前
网络安全 | F5-Attack Signatures-Set详解
网络·数据库·web安全
Ronin-Lotus2 小时前
上位机知识篇---CMake
c语言·c++·笔记·学习·跨平台·编译·cmake
酱学编程2 小时前
【计算机网络】公有和私有 IP 地址
tcp/ip·计算机网络·智能路由器
gyeolhada3 小时前
计算机组成原理(计算机系统3)--实验五:处理器结构实验二
stm32·单片机
Zfox_3 小时前
HTTP cookie 与 session
linux·服务器·网络·c++·网络协议·http