【保姆级IDF】ESP32使用WIFI作为STA模式与其他设备进行TCP通信

Tips:

抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。

AP模式下的通信参看我其他文章:

【保姆级IDF】ESP32使用WIFI作为AP模式TCP通信:连接客户端+一对多通信

目录

1.TCP协议简介

2.Socket简介

3. 代码思路

4. 具体代码说明

4.1 Socket的创建

4.2 Socket的连接

4.3 网络数据的发送和接收

4.4 setsockopt函数介绍

1.设置超时选项

2.启用或禁用套接字广播

3.设置套接字缓冲区大小

4.设置套接字重用选项

5.设置TCP选项(例如TCP_NODELAY)

5.成果展示

6.总结


1.TCP协议简介

        传输控制协议(英语:Transmission Control Protocol,缩写:TCP),是一种面向连接的、可靠的、基于字节流的传输层传通信协议,由IETF的RFC 793定义。说到TCP,我们都不陌生,许多网络通信的方式都是以TCP协议作为底层协议在运行,比如我们常常使用的微信,在与对方的文字聊天过程中,每一条消息的发送都是以TCP协议作为基础在手机上传输数据,还有一些网络协议比如PTP/IP,他的底层也是TCP协议在实现。TCP协议它显著的特点就是在传输过程中信道的稳定性极好,确保数据不会因意外而丢失。

        它在建立以及断开连接时的原理我们可以简单的理解为:三次握手四次挥手。在建立连接时会有三次握手:

        1.第一次握手由客户端发送资源包给到服务端,若该过程正常,则得出结论:服务端接收、客户端发送服务正常。

        2.第二次握手由服务端发送资源包给到客户端,若该过程正常,则得出结论:服务端发送、客户端接收服务正常。

        3.第三次握手由客户端发送资源包给到服务端,若该过程正常,则得出结论:服务端接收、客户端发送服务正常。

        这里大家可能就会有疑问了?为什么还需要进行第三次握手呢?前两次不是已经得出结论客户端、服务端接收、发送资源包能力正常了吗?其实并不是,第一、二次握手只是在单独的过程中得出服务正常的结论,但是在第二次握手结束后,服务端的接收能力和客户端的发送能力未知,这时候便有了TCP的第三次握手过程。在三次握手之后,便确定双方有可以自主通信的能力,就可以相互传输数据。

        当数据传输任务完成后,客户端需要关闭连接时,双方就会执行四次挥手的过程:

        1.客户端发起第一次挥手后,会向服务端发送断开连接请求报文。

        2.服务端收到请求后,同样会向客户端发送一条报文,这是第二次挥手

        3.服务端再次发送一个请求关闭连接的报文,这是第三次挥手

        4.客户端发送一个回应报文给服务端,服务端收到后随即进入关闭状态,而客户端也在等待一段时间后关闭,这是第四次挥手

        以上只是一个非常简单的比喻说明,实际过程中,TCP协议使用的数据报文有不同类型,若想查看更多详细的内容,读者可自行上网搜索,百度百科、CSDN有很多文章介绍的非常详细,博主在此便不班门弄斧赘述太多了,况且ESP32的库非常完善,我们在使用过程中无需关心TCP协议的实现过程,仅需调用乐鑫提供的库函数即可完成三次握手、四次挥手。

2.Socket简介

        Socket,即套接字,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。用大白话讲就是Socket在代码中属于文件描述符,在你创建并将Socket连接好目标设备之后,想要和目标设备通信,只需要将你想要发送的数据全部写进Sokcet这个文件描述符里面就可以了,至于数据怎么传输,由底层的TCP协议负责,我们无需关心。

        和TCP协议一样,Socket在网上也有很多文章在介绍,以下博主只简单说明自己对Socket的理解。

        在TCP通信中,Socket扮演的角色至关重要。如果把自己这个人比作要传输的数据,那么你想去找好朋友,比如说他在美国洛杉矶,你就需要坐飞机过去,那在这中间你这个人就是要传输的数据,Socket就是机场+航线,说他是航线是因为它相当于数据通信的通道,说他也是机场是因为Socket自身也有一个数据缓冲区用来暂存数据,同时也有自己的一些属性可以设置;Socket的一些参数比如:要连接的对方IP地址,端口号,协议簇,IP地址相当于你的目的地美国,端口号相当于你到了美国之后你需要去到你的好朋友所在的城市比如洛杉矶,协议簇则相当于你乘坐飞机的机型(波音or空客)

        以上的比喻可能不太恰当,但“你”作为一个数据,大致上就是这么传输的。网上也有很多对Socket的比喻,读者可自行查阅,看自己的喜好怎么去理解。


        废话不多说,下面谈谈怎么编写ESP32的代码来实现这么一个利用Socket进行网络通信的功能。

3. 代码思路

        在确保ESP32已成功连接目标设备的WIFI热点后,我们就可以着手创建Socket了。

        1.使用socket函数创建一个套接字(Socket)。

        2.配置好Socket连接需要的结构体,然后调用connect函数并将这个结构体作为参数即可连接。

        3.使用函数将你想要发送的数据(可以是字符串或者结构体等等)写进Socket即可。接收同理,使用函数将Socket缓冲区数据读取出来放进我们创建的数组中。

        详细的说明会在下文提到,注意这里第二步,ESP32作为STA模式,只需要创建Socket后和目标设备成功连接即可进行通信,如果作为AP模式则多两步,就是需要监听(listen)和绑定(bind),这里不多介绍,以后会专门写这方面的文章。

4. 具体代码说明

        这里博主为了方便,直接使用了IDF的softSTA例程代码,在已连接WIFI的基础上介绍套接字的使用方法,顺带注意头文件的包含。

        如果不清楚怎么连接WIFI,可以看我上一篇文章

        ESP32使用WIFI作为STA模式实现:WIFI扫描串口输出+串口输入指定WIFI名称和密码+连接WIFI

4.1 Socket的创建

int create_socket()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
    //socket函数创建成功时的返回值是文件描述符,如果返回-1,则表示创建失败
    //socket三个参数分别是:地址类型(AF_INET表示IPv4)、套接字类型(SOCK_STREAM表示TCP)、协议(0表示默认协议)
    if (sockfd < 0) {//若返回值为-1,则表示创建失败,处理创建失败的情况
        perror("socket");//以socket:形式输出错误信息
        exit(EXIT_FAILURE);//退出
        return -1;
    }
    return sockfd;//返回创建成功的文件描述符
}

        这里使用一个函数对Socket的创建进行了封装,第一个函数:socket就是创建套接字的函数,他的函数原型是:int socket(int domain, int type, int protocol);其中第一个参数domain表示通信方式,有10个可选参数,它的参数范围如下:

名称含义名称含义
PF_UNIX,PF_LOCAL本地通信PF_X25ITU-T X25 / ISO-8208协议
AF_INET,PF_INETIPv4 Internet协议PF_AX25Amateur radio AX.25
PF_INET6IPv6 Internet协议PF_ATMPVC原始ATM PVC访问
PF_IPXIPX-Novell协议PF_APPLETALKAppletalk
PF_NETLINK内核用户界面设备PF_PACKET底层包访问

        这里我们选择填入AF_INET,也就是IPv4协议。第二个参数type则表示通信类型,有6个可选参数,它的参数范围如下:

名称含义
SOCK_STREAMTcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM支持UDP连接(无连接状态的消息)
SOCK_SEQPACKET序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAWRAW类型,提供原始网络协议访问
SOCK_RDM提供可靠的数据报文,不过可能数据会有乱序
SOCK_PACKET这是一个专用类型,不能呢过在通用程序中使用

        这里我们选择流式传输,填入SOCK_STREAM,第三个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。所以我们这里填0.

        填入好参数后,socket函数就会执行创建套接字的操作,如果操作成功了,那么将会返回一个文件描述符,我们用自己定义的int类型变量sockfd来承载这个文件描述符;但如果失败了,那么socket函数就会返回-1,可以通过返回值来判断创建是否成功。于是我们要准备对其做错误处理,如果返回值判断为-1,那么就打印错误并退出,若成功则不进入if判断直接返回这个文件描述符。

4.2 Socket的连接

void connect_to_server(int sockfd) {
    struct sockaddr_in server_addr;//创建一个结构体变量,用于存储服务器的地址信息
    memset(&server_addr, 0, sizeof(server_addr));//清零初始化
    server_addr.sin_family = AF_INET;//设置地址族为IPv4
    server_addr.sin_port = htons(8080);//设置端口号为8080
    inet_pton(AF_INET, "192.168.0.107", &server_addr.sin_addr); //将点分十进制的IP地址转换为二进制形式,并存储在server_addr.sin_addr中

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {//连接服务器
        perror("connect");//以connect:形式输出错误信息
        closesocket(sockfd);//关闭套接字
        exit(EXIT_FAILURE);//退出
        vTaskDelay(6 / portTICK_PERIOD_MS);//延时6ms
    }
}

        首先创建一个用于存储服务器地址信息的结构体变量server_addr,他的类型是sockaddr_in,这个类型不需要我们自己定义,库中已有。然后使用memset清零初始化,然后对结构体成员赋值,主要就是三个成员,分别是协议簇(sin_family)、端口号(sin_port)和服务端的IP地址,我们选用IPv4协议,所以sin_family赋为AF_INET,端口号需要根据服务端的端口号来填写,这里我仅在电脑上做测试用,所以我在电脑上的网络助手设置了端口号为8080,于是port赋为8080,IP地址有点特殊,需要使用inet_pton函数将IP地址的格式转化为二进制,该函数第一个参数表示需要转换的IP地址属于哪一类IP地址,由于我们使用IPv4协议,所以IP地址自然是IPv4地址,所以第一个参数选择为AF_INET,第二个参数就是需要转换的IP地址,我们以字符串的形式填入,即:"192.168.0.107",最后一个参数就是转化之后的IP地址存放的地方,我们选择server_addr结构体的sin_addr成员作为存放的地方。这样,我们就将服务器地址信息配置好了,方便后面套接字连接时调用。

        然后就是用connect函数连接套接字与服务端,connect函数第一个参数是需要连接的套接字,我们刚刚创建了sockfd,所以将sockfd作为第一个参数,第二个参数则是刚刚配置好的服务端地址信息结构体,但是注意,这里需要进行类型强制转换,以适配connect函数的参数类型。所以第二个参数完整表示为:(struct sockaddr *)&server_addr,第三个参数就是这个结构体的大小,我们直接调用C库函数sizeof计算得出,所以第三个参数为:sizeof(server_addr)。这样一来,我们就让套接字连接服务端了。同样的,connect也会返回成功和失败的两种返回值,如果连接成功,connect则返回0,连接失败则返回-1,所以同样进行错误处理,若连接失败则打印错误信息,并关闭套接字后退出。随后短暂延时6ms。

4.3 网络数据的发送和接收

        博主这里只简单发送两句话,读者看完应该可以掌握如何用Socket发送网络数据了

uint8_t buffer[1024];//接收数据缓冲区
while(1)
    {
        err = send(sockfd, "hello ESP32!", 12, 0);//通过套接字向服务器发送网络数据:hello ESP32!
        printf("send %d bytes to server: %s\n", err, "hello ESP32!");//串口打印调试信息,表示发送成功
        vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1s
        err = send(sockfd, "hello world!", 12, 0);//通过套接字向服务器发送网络数据:hello world!
        printf("send %d bytes to server: %s\n", err, "hello world!");//串口打印调试信息,表示发送成功
        err = recv(sockfd, buffer, sizeof(buffer), 0x08);//通过套接字接收服务器发送的网络数据
        if(err > 0)//判断是否有接收到数据,如果有则打印,没有则忽略
        printf("recv %d bytes from server: %s\n", err, buffer);//串口打印调试信息,表示接收成功
    }

        Socket要想发送数据,就需要使用send函数,这是套接字常用的发送函数,也就是我上文提到的"使用函数往Socket里面写数据"的意思。send函数有四个参数,第一个参数就是选择你想要使用的套接字名称,这里我使用的是sockfd,所以填入sockfd;第二个参数是你想要发送的数据,这里我发送hello ESP32!我们以字符串形式填入即可;第三个参数是你想要发送的数据的长度,我这里包括空格一共12个字节,所以填入12,最后一个参数是发送的形式,send函数中这一项参数我们一般填入0即可满足90%的需求。(额外提一点,send函数可以发送很多东西,读者可以自行开发,比如结构体,定义好结构体后,将第二个参数替换为:&struct即可,&是为了取地址,需要知道这个结构体变量的首地址,struct就是你创建的结构体变量名字)

        接收数据函数:recv(),和send函数几乎差不多。第一个参数同send函数第一个参数,第二个参数是指定接收到的数据的存放地方,我们一般称缓冲区,第三个参数是你想要接收的字节数,我这里就直接写了数组buffer的大小,意味着接收和buffer大小同样的数据。最后一个参数是接收的方式,这里我介绍两种常用的方式:阻塞式和非阻塞式。当填入0时即为阻塞式接收,意思就是,当我执行recv时,就去Socket缓冲区找数据,如果有数据,立即接收,随后立即返回。如果没有数据,则在此等待,注意是死等,一直等到有数据来后,接收完再返回,返回值是本次接收到的数据长度。当填入0x08时,就是非阻塞式接收,就是我执行recv时,就去缓冲区找数据,如果有,立即接收后立即返回,返回值是本次接收到的数据长度,如果没有,也立即返回,只不过返回值可能是-1或0。err变量的作用是保存send函数或者recv函数的返回值,他提供了send和recv函数的工作状况,告诉我们send函数发送了多少字节,是否发送失败;recv函数接收了多少字节,是否接收失败。recv函数有一点注意,你设置的接收数据长度,也就是第三个参数可以大于Socket缓冲区实际的数据量,此时会将数据全部接收完并返回实际接收的数据长度。

        这里我是使用的是非阻塞式接收,因为程序在死循环当中,无法确定电脑何时发来消息,如果使用阻塞式接收,那么程序就会因为recv死等数据而卡住。最后判断recv的返回值来确定是否有数据接收到,有则串口打印接收到的数据。

        这里printf函数就是往串口打印信息,用法和C库函数一模一样。乐鑫的库已经提前重定向好了这个函数,我们直接使用即可!

4.4 setsockopt函数介绍

        前面提到飞机场的比喻时说到Socket也有自己的一些属性可以设置,那么设置方法就是使用setsockopt函数。下面看它的函数原型:

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd:是套接字文件描述符,用于标识要设置选项的套接字。
- level:指定选项的级别,通常使用 SOL_SOCKET、SOL_TCP 或 SOL_UDP 等,具体取决于所设置选项的类型。
- optname:指定要设置的选项的名称,如缓冲区大小、超时设置、广播选项等。
- optval:是一个指向存储选项值的缓冲区的指针。
- optlen:是 optval 缓冲区的长度。

以下是一些常见的套接字选项以及它们的用法:

1.设置超时选项

int timeout = 5000; // 5秒
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

这会设置接收操作的超时时间为5秒。

2.启用或禁用套接字广播

int broadcast = 1; // 启用广播
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

这会启用套接字的广播功能。

3.设置套接字缓冲区大小

int buffer_size = 8192;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size));

这会设置接收缓冲区大小为8192字节。

4.设置套接字重用选项

int reuse = 1; // 启用套接字重用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

这会启用套接字地址重用,允许多个套接字绑定到相同的地址。

5.设置TCP选项(例如TCP_NODELAY)

int tcp_nodelay = 1; // 启用TCP_NODELAY
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &tcp_nodelay, sizeof(tcp_nodelay));

这会启用TCP无延迟选项,用于减少延迟。

        需要注意的是,不同的操作系统和套接字类型(如TCP套接字和UDP套接字)可能支持不同的选项。在使用 setsockopt() 函数时,务必查阅相关的系统文档或套接字编程文档,以确保正确设置选项。此外,错误处理也非常重要,以确保 setsockopt() 函数的调用是否成功。

        setsockopt函数若想调用,请在创建好套接字后再调用,否则也没有创建好的套接字供我们设置。

4.5 main函数展示

头文件注意一下,如果有缺少记得补上,我这里没有报错。

#include <string.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 "lwip/sockets.h"
#include "lwip/errno.h"

#include "lwip/err.h"
#include "lwip/sys.h"

/* The examples use WiFi configuration that you can set via project configuration menu

   If you'd rather not, just change the below entries to strings with
   the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID      "此处换成你们要连接的WIFI名称"
#define EXAMPLE_ESP_WIFI_PASS      "此处换成你们要连接的WIFI的密码"
#define EXAMPLE_ESP_MAXIMUM_RETRY  CONFIG_ESP_MAXIMUM_RETRY

#if CONFIG_ESP_WPA3_SAE_PWE_HUNT_AND_PECK
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_HUNT_AND_PECK
#define EXAMPLE_H2E_IDENTIFIER ""
#elif CONFIG_ESP_WPA3_SAE_PWE_HASH_TO_ELEMENT
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_HASH_TO_ELEMENT
#define EXAMPLE_H2E_IDENTIFIER CONFIG_ESP_WIFI_PW_ID
#elif CONFIG_ESP_WPA3_SAE_PWE_BOTH
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_BOTH
#define EXAMPLE_H2E_IDENTIFIER CONFIG_ESP_WIFI_PW_ID
#endif
#if CONFIG_ESP_WIFI_AUTH_OPEN
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_OPEN
#elif CONFIG_ESP_WIFI_AUTH_WEP
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WEP
#elif CONFIG_ESP_WIFI_AUTH_WPA_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WAPI_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WAPI_PSK
#endif

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

/* The event group allows multiple bits for each event, but we only care about two events:
 * - we are connected to the AP with an IP
 * - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

static const char *TAG = "wifi station";

static int s_retry_num = 0;

uint8_t buffer[1024];

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"connect to the AP fail");
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void)
{
    s_wifi_event_group = xEventGroupCreate();

    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_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            /* Authmode threshold resets to WPA2 as default if password matches WPA2 standards (pasword len => 8).
             * If you want to connect the device to deprecated WEP/WPA networks, Please set the threshold value
             * to WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK and set the password with length and format matching to
             * WIFI_AUTH_WEP/WIFI_AUTH_WPA_PSK standards.
             */
            .threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD,
            .sae_pwe_h2e = ESP_WIFI_SAE_MODE,
            .sae_h2e_identifier = EXAMPLE_H2E_IDENTIFIER,
        },
    };
    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() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
     * happened. */
    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}

int create_socket()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
    //socket函数创建成功时的返回值是文件描述符,如果返回-1,则表示创建失败
    //socket三个参数分别是:地址类型(AF_INET表示IPv4)、套接字类型(SOCK_STREAM表示TCP)、协议(0表示默认协议)
    if (sockfd < 0) {//若返回值为-1,则表示创建失败,处理创建失败的情况
        perror("socket");//以socket:形式输出错误信息
        exit(EXIT_FAILURE);//退出
        return -1;
    }
    return sockfd;//返回创建成功的文件描述符
}

void connect_to_server(int sockfd) {
    struct sockaddr_in server_addr;//创建一个结构体变量,用于存储服务器的地址信息
    memset(&server_addr, 0, sizeof(server_addr));//清零初始化
    server_addr.sin_family = AF_INET;//设置地址族为IPv4
    server_addr.sin_port = htons(8080);//设置端口号为8080
    inet_pton(AF_INET, "192.168.0.107", &server_addr.sin_addr); //将点分十进制的IP地址转换为二进制形式,并存储在server_addr.sin_addr中

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {//连接服务器
        perror("connect");//以connect:形式输出错误信息
        closesocket(sockfd);//关闭套接字
        exit(EXIT_FAILURE);//退出
        vTaskDelay(6 / portTICK_PERIOD_MS);//延时6ms
    }
}

void app_main(void)
{
    //Initialize 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());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    wifi_init_sta();//初始化wifi连接

    int err;
    int sockfd = create_socket();//创建套接字
    connect_to_server(sockfd);//将套接字连接服务器
    vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1s

    while(1)
    {
        err = send(sockfd, "hello ESP32!", 12, 0);//通过套接字向服务器发送网络数据:hello ESP32!
        printf("send %d bytes to server: %s\n", err, "hello ESP32!");//串口打印调试信息,表示发送成功
        vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1s
        err = send(sockfd, "hello world!", 12, 0);//通过套接字向服务器发送网络数据:hello world!
        printf("send %d bytes to server: %s\n", err, "hello world!");//串口打印调试信息,表示发送成功
        err = recv(sockfd, buffer, sizeof(buffer), 0x08);//通过套接字接收服务器发送的网络数据
        if(err > 0)
        printf("recv %d bytes from server: %s\n", err, buffer);//串口打印调试信息,表示接收成功

    }

}

5.成果展示

1:选择协议类型,当ESP32作为STA模式时连接电脑,电脑自然就是服务端

2:设置本机IP地址,建议设置自己电脑的地址,如不知道自己IP可以win+R,输入cdm然后输入ipconfig即可查询

3:设置电脑的端口号,ESP32的代码中设置的端口号和它一致

4:配置好后点击连接,再次点击就是断开

        ESP32串口打印是辅助调试,方便我们看到ESP32的运行情况。

        下面附上一张全图

        其实只要保证ESP32和通信设备在同一局域网即可实现通信!

6.总结

        以上就是如何利用Socket建立TCP通信的方法。ESP32作为STA模式时这还算是比较简单的,只需要创建连接就可通信,调用函数底层库会帮我们做三次握手四次挥手的操作。后续用作AP模式下建立TCP通信的方法以后会更新。
        以上代码博主经过实测可用稳定,如有疑问欢迎留言交流。以上观点为个人理解,只想抛砖引玉,如有不对的地方欢迎指正,不喜轻喷。


2024.10.21-21:45

如果有收获的话还可以支持一下作者,你的一键三连就是对我最大的肯定!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_山岚_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值