4·ESP32-C3入门教程——从本地控制走向云端控制(TCP/IP UDP篇)

        距离实现一个完整的物联网小应用只差最后一步了,今天聊聊怎么样在手机上对ESP32芯片发送指令和接收数据,并借助ESP官方的接口——rainmaker,来实现远程控制和通信。我们也借由此进入智能家居时代1.0(部分物联网概念可以看看【序】在23年谈物联网

       

目录

level 1:通过socket广播收发实现本地控制

建立TCP SCOKET CLIENT通信

建立TCP SCOKET SEVER通信

小结

level 2:更广泛的传输--UDP通信 & 通过远程控制实现点灯

总结


        虽然在上一篇中我们已经学习到了如何让ESP32-C3通过WiFi连接互联网,以及如何通过UDP广播的方式通过手机上的esp touch为ESP32轻松配置网络(链接指路→ESP32 从scan到smart config 讲透WIFI配置)但我们仍然需要更进一步,如果把互联网比作是不同端口之间的路线的话,处理器如何判断哪些数据是需要的,哪些数据是不需要的呢?这是我们今天所想要解决的问题。

        所以这里我们就需要更进一步引入协议的概念。一般情况下基于WiFi和以太网的设备都会原生运行我们比较熟知的互联网TCP/IP协议栈,通过它,我们可以大大降低数据本身协议的适配和开发难度(but 虽然降低了,对于入门来说仍然有很多东西要学)

        TCP即传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议,分为服务器和客户端。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

TCP通信的框图

level 1:通过socket广播收发实现本地控制

        开始广播收发之前,我们需要下载一个网络调试工具(NetAssist)的软件。可以帮助我们调节端口的位置地址以及相关的参数的,我们可以通过它来进行内容的收发。下载链接指路(链接源于网络):NetAssist网络调试助手.exe

         安装完成之后打开会看到如下的界面:

        

        其实乍一看SOCKET概念的时候,我还是有一点懵的,因为之前没有做过网络通信相关的实验,花了好一阵子才开始理清整体的结构和概念,上面的图一定要结合代码多看几遍(当然,我下面的注释也会按照层级来详细解释)

        首先要解释一下这个client是什么,在上面TCP通信的框图里面,我们看到了左半部分作为客户端,右半部分作为服务端;connect之后客户端可以直接提交内容和接受信息,但服务端在收发信息之前,需要bind listen accept确立状态。

        所以我们先从左半边相对简单的的客户端去分析~

建立TCP SCOKET CLIENT通信

       首先我们从整体上对整个代码的概念进行认识,因为我们esp是client模式,所以要先去看电脑端的 网络调试助手选择的ip和端口,并且填入到最上面这里#define,不然则会无法配对。关于函数部分的详细原理会在后面的小结部分梳理,先从整体上对各个功能模块认知,建立宏观的基础概念更有利于理解(个人看法)。

源代码如下:

#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "addr_from_stdin.h"
#include "lwip/err.h"
#include "lwip/sockets.h"



#define HOST_IP_ADDR    "192.168.0.133"  
#define PORT            3333

static const char *TAG = "example";
static const char *payload = "message from computer";

static void tcp_client_task(void *pvParameters)
{
    char rx_buffer[128];
    char host_ip[] = HOST_IP_ADDR;
    int addr_family = 0;
    int ip_protocol = 0;

    while (1)   //大while(1)
    {
        struct sockaddr_in dest_addr;
        dest_addr.sin_addr.s_addr = inet_addr(host_ip);
        dest_addr.sin_family = AF_INET;     //调用了另一个文件中的 #define AF_INET 2
        dest_addr.sin_port = htons(PORT);   //port 上面咱们定义过啦
        addr_family = AF_INET;              //(同上)
        ip_protocol = IPPROTO_IP;           //#define IPPROTO_IP 0;


        int sock =  socket(addr_family, SOCK_STREAM, ip_protocol);
        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, PORT);

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

        while (1) //大while(1)里面的while(1)
        {
            int err = send(sock, payload, strlen(payload), 0);
            if (err < 0) {
                ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
                break;
            }

            int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
            // Error occurred during receiving
            if (len < 0) {
                ESP_LOGE(TAG, "recv failed: errno %d", errno);
                break;
            }
            // Data received
            else {
                rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
                ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
                ESP_LOGI(TAG, "%s", rx_buffer);
            }

            vTaskDelay(2000 / portTICK_PERIOD_MS);
        }

        if (sock != -1) {
            ESP_LOGE(TAG, "Shutting down socket and restarting...");
            shutdown(sock, 0);
            close(sock);
        }
    }
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(example_connect());

    xTaskCreate(tcp_client_task, "tcp_client", 4096, NULL, 5, NULL);
}

        因为调用了其他文件夹的文件,所以需要修改一下顶层的cmakelist(注意,不是main文件夹里面的),这样可以链接到通信所需要用到的两个头文件。

cmake_minimum_required(VERSION 3.5)  
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mqtt_tcp)

建立TCP SCOKET SEVER通信

        作为SERVER端,其实代码中需要考虑的东西是更多的,不能仅仅看作只增加了bind()、listen()和accept();不过如果client部分有没看懂的部分也没关系,可能学完sever部分会有新的观念,还是老规矩,我们先从整体的角度出发:

源代码如下:

#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>


#define PORT                        3333
#define KEEPALIVE_IDLE              5
#define KEEPALIVE_INTERVAL          5
#define KEEPALIVE_COUNT             3

static const char *TAG = "tcp server";

void wifi_get_ip(void)
{
    tcpip_adapter_ip_info_t ipInfo;
    tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);
    ESP_LOGI(TAG, "wifi_get_ip ip=%s", ip4addr_ntoa(&(ipInfo.ip.addr)));
}

static void do_retransmit(void *pvParameters)
{
    int sock = (int)pvParameters;
    int len;
    char rx_buffer[128];

    ESP_LOGI(TAG, "do_retransmit(%d)", sock);
    while(true)
    {
        len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
        if (len < 0) {
            ESP_LOGE(TAG, "Socket(%d) Error occurred during receiving: errno %d", sock, errno);
            shutdown(sock, 0);
            close(sock);
            vTaskDelete(NULL);
        } 
        
        else if (len == 0) {
            ESP_LOGW(TAG, "Socket(%d) Connection closed", sock);
        }
        
        else {
            rx_buffer[len] = 0; // 空中止接收到任何内容都视为字符串
            ESP_LOGI(TAG, "Socket(%d) Received %d bytes: %s", sock, len, rx_buffer);

            // send() 可以返回定义长度更短的字符,所以有一点裕度更好(个人翻译的,不一定准确qwq)
            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, "Socket(%d) Error occurred during sending: errno %d", sock, errno);
                }
                to_write -= written;
            }
        }
    }
}

static void tcp_server_task(void *pvParameters)
{
    char addr_str[128];
    int addr_family = (int)pvParameters;
    int ip_protocol = 0;
    int keepAlive = 1;
    int keepIdle = KEEPALIVE_IDLE;
    int keepInterval = KEEPALIVE_INTERVAL;
    int keepCount = KEEPALIVE_COUNT;
    struct sockaddr_storage dest_addr;

    wifi_get_ip();
    if (addr_family == AF_INET) {
        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(PORT);
        ip_protocol = IPPROTO_IP;
    }

    int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
    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");

    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", addr_family);
        goto CLEAN_UP;
    }
    ESP_LOGI(TAG, "Socket bound, port %d", PORT);

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

    while (1) {
        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
        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 | %d", addr_str, sock);

        // do_retransmit(sock);
        xTaskCreate(do_retransmit, "do_retransmit", 4096, (void*)sock, 6, NULL);
    }

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

void app_main(void)
{
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    example_connect();

    xTaskCreate(tcp_server_task, "tcp_server", 4096, (void*)AF_INET, 5, NULL);  
}

小结

整体梳理一下所用到的函数部分,其实不难看出几乎每一个步骤都是围绕着socket操作的。

socket创建

socket绑定

socket监听

socket连接

发送数据

接受数据

socket关闭

socket释放

level 2:更广泛的传输--UDP通信 & 通过远程控制实现点灯

        UDP 是 User Datagram Protocol 的简称, 中文名是用户数据报协议, 是 OSI (Open System Interconnection, 开放式系统互联)参考模型中一种无连接的传输层协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。

        在 OSI 模型中,UDP和TCP一样,在传输层(第四层),处于 IP 协议的上一层。UDP 协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前 8 个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

        简单来说UDP就是没有确TCP协议。TCP每发出一个数据包都要求确认,如果有一个数据包丢失,就收不到确认,发送方就必须重发这个数据包。为了保证传输的可靠性,TCP协议在UDP基础之上建立了三次对话的确认机制,即在正式收发数据前,必须和对方建立可靠的连接。TCP数据包和UDP一样,都是由首部和数据两部分组成,唯一不同的是,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

        UDP 用来支持那些需要在计算机之间传输数据的网络应用。 包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用 UDP 协议。 UDP 协议从问世至今已经被使用了很多年, 虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天 UDP 仍然不失为一项非常实用和可行的网络传输层协议。

        下图是UDP的传输过程,可以看到相比于TCP协议,UDP简化了一些步骤:

UDP传输过程

        这一部分原理和TCP部分大致相同,因为网上找到的例程有点小bug还没有解决,所以暂时先简单分享一下UDP方面的思路,后续理解了会更新完整代码,关键实现代码如下(led灯在另一个文件中已经定义,此处也可以删掉):

参考代码如下:

static struct sockaddr_in dest_addr;                  //远端地址
socklen_t dest_addr_socklen = sizeof(dest_addr);
static int udp_socket = 0;                          //连接socket
TaskHandle_t xUDPRecvTask = NULL;

void udp_send_data(char* data, int len)
{

    if(udp_socket>0){

        int err = sendto(udp_socket, data, len, 0, (struct sockaddr *)&dest_addr, dest_addr_socklen);
        if (err < 0)   printf( "Error occurred during sending: errno %d", errno);
    }
}

void udp_recv_data(void *pvParameters){
    socklen_t socklen = sizeof(dest_addr);
    uint8_t rx_buffer[1024] = {0};
    printf("create udp recv\n");

    while (1)
    {
        int len = recvfrom(udp_socket, rx_buffer, sizeof(rx_buffer) - 1, 0,  (struct sockaddr *)&dest_addr, &dest_addr_socklen);
        if(len > 0){
            if(len == 2 && rx_buffer[0]=='o' && rx_buffer[1]=='n')  led_red(LED_ON);
            else if(len == 3 && rx_buffer[0]=='o' && rx_buffer[1]=='f' && rx_buffer[2]=='f')     led_red(LED_OFF);
            else
            {
                rx_buffer[len] = 0; //未尾增加"\0,确保长度"
                printf("Received %d bytes: %s.\n", len, rx_buffer);
                udp_send_data((char*)rx_buffer, len);
            }
        }
    }
}

void udp_ini_client(void *pvParameters){
    if(udp_socket>0){
        close(udp_socket);
        udp_socket=0;
    }
    udp_socket = socket(AF_INET,SOCK_DGRAM,0);
    printf("connect_socket:%d\n",udp_socket);

    if(udp_socket < 0){
       printf( "Unable to create socket: errno %d", errno);
       return;
    }

    dest_addr.sin_addr.s_addr = inet_addr("255.255.255.255"); 
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(UDP_PORT);//目标端口
    printf("Socket created, sending to 255.255.255.255:%d", UDP_PORT);

    struct sockaddr_in Loacl_addr;
    Loacl_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    Loacl_addr.sin_family = AF_INET;
    Loacl_addr.sin_port = htons(UDP_PORT); 

    uint8_t res = 0;
    res = bind(udp_socket,(struct sockaddr *)&Loacl_addr,sizeof(Loacl_addr));
    if(res != 0){
        printf("bind error\n");
    }

    if(xUDPRecvTask != NULL){
        vTaskDelete(xUDPRecvTask);
        xUDPRecvTask = NULL;
    }

    xTaskCreate(&udp_recv_data,"udp_recv_data",2048*2,NULL,10,&xUDPRecvTask);
    vTaskDelete(NULL);
}

void create_udp()
{
    xTaskCreate(&udp_ini_client, "udp_ini_client", 4096, NULL, 5, NULL);
}

总结

        对于第一次接触本地控制来说,tcp/ip真的可以算是一个难啃的骨头,会有很多新的概念,会综合很多部分的使用;最重要的是,不同例程的思路也迥然不同,官方的例程不够全面也没有注解,而第三方的例程有时候的定义和用法又需要重新理解,还有一些嵌套的思路有时候就像解一团绳结一样,如果不是对整体特别熟练,理解起来也会非常头大。

        从全局的角度一点点入手,分解、拆开之后,会清晰很多;有些小块不懂的地方其实也不用死磕,可以先记录下来,之后再一点点看,慢慢会有一些思路,很多问题在不知不觉中就明白了。

        这章主要还是聊的局域网内的本地控制,现在我们在同一个wifi下已经能通过电脑远程控制板子了,再加上之前的smart config,可以做出一些不错的尝试。下一章更近一步,通过MQTT协议和HTTP协议的学习,获得联网信息,再到接入esp rainmaker,达到远程控制。
        

猜你喜欢

转载自blog.csdn.net/TenYao_/article/details/128891378