基于socket的esp32远程通信

前言

        一般提到esp32远程通信首先想到的是mqtt协议,那为什么会有这篇文章,因为在一些需要实时性控制的场景下,我认为mqtt的延迟可能会太大而不适合。之前我做过一个基于mqtt远程控制小车的小玩意,但我发现延迟太大,可能是我用的是阿里云的公共物联网实例的缘故,也有可能是本身它就会慢一些,当然我也没本地使用开源的mqtt服务端去验证,总之接下来将呈现我学习基于socket的esp32远程通信的过程,有什么不对还请指正。

socket的介绍

        “Socket”(套接字)是一种计算机网络编程中的通信接口,用于在不同设备或进程之间建立通信。它允许程序在不同的网络节点之间发送和接收数据,是网络编程的基础组件之一。它是以tcp,udp为基础。

服务端

一、与socket相关的函数

创建Socket:使用socket()函数。

int socket(int domain, int type, int protocol);

domain:指定协议族(地址族),常用的有:

    AF_INET:IPv4协议族。

    AF_INET6:IPv6协议族。

    AF_UNIX:本地通信(仅限于同一台机器)。

type:指定Socket的类型,常用的有:

    SOCK_STREAM:流式Socket(TCP)。

    SOCK_DGRAM:数据报Socket(UDP)。        

    SOCK_RAW:原始Socket。

protocol:指定协议。通常设置为 0,表示使用默认协议。例如:

              若typeSOCK_STREAM,默认协议是 TCP。

              若 typeSOCK_DGRAM,默认协议是 UDP。

返回值

              成功时返回一个非负整数,表示Socket文件描述符(类似于文件句柄)。

失败时返回 -1,并设置 errno

绑定地址和端口:使用bind()函数。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:Socket文件描述符,由socket()函数创建。

addr:指向sockaddr结构的指针,包含要绑定的地址和端口信息。

addrlenaddr结构的大小,通常使用sizeof()来获取。

监听连接:使用listen()函数。

int listen(int sockfd, int backlog);

sockfd:Socket文件描述符,由socket()函数创建并绑定到本地地址和端口。

backlog:指定未完成连接队列的最大长度。它是一个建议值,实际长度由系统决定。

接受连接:使用accept()函数。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:  监听Socket的文件描述符,由socket()创建并经过listen()设置为监听状态。

addr:      指向sockaddr结构的指针,用于存储发起连接的客户端的地址信息。如果不需要                     客户端的地址信息,可以传入NULL

addrlen:指向一个socklen_t类型的指针,表示addr结构的大小。在调用accept()之前,需                   要将其设置为addr结构的大小(如sizeof(struct sockaddr_in))。调用后,                       addrlen会被更新为实际存储的地址信息的大小。

数据通信:使用send()和recv()函数。

send():

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd:Socket文件描述符,标识要发送数据的Socket。

buf:     指向要发送的数据的缓冲区。

len:     要发送的数据的长度(以字节为单位)。

flags: 控制发送行为的标志。通常设置为0,表示正常发送。

返回值

              成功时返回实际发送的字节数(可能小于请求的字节数)。

失败时返回-1,并设置errno

recv():

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sockfd: Socket文件描述符,标识要接收数据的Socket。

buf:      指向存储接收数据的缓冲区。

len:      缓冲区的大小(以字节为单位)。

flags:  控制接收行为的标志。通常设置为0,表示正常接收。

返回值

               成功时返回实际接收到的字节数。如果对端关闭了连接,返回0。失败时返回-1,                   并设置errno

关闭连接:使用close()函数。

int close(int fd);

fd:要关闭的文件描述符(对于Socket编程,是Socket文件描述符)。

返回值

        成功时返回0。失败时返回-1,并设置errno

二、与多线程相关的函数 

这里用到的是pthread,它在pthread.h里。

创建子线程:使用pthread_create()函数。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine)(void*), void *arg);

thread:指向线程标识符的指针。线程创建后,线程的 ID 会存储在这个变量中。pthread_t                 是线程的唯一标识符,通常用于后续的线程操作(如等待线程结束)。

attr: 指定线程属性的指针。如果不需要设置特殊属性,可以传入 NULL,使用默认属                       性。线程属性包括线程的堆栈大小、分离状态等。

void *(*start_routine)(void*):最主要的就是这一坨!
               指定线程的入口函数。这是一个函数指针,指向线程开始执行的函数。该函数必须                 接受一个 void* 类型的参数,并返回一个 void* 类型的值。

arg:  传递给线程入口函数的参数。如果不需要传递参数,可以传入 NULL。如果需要传                   递多个参数,可以将它们封装到一个结构体中,并将结构体的指针传递给线程函数。 

分离子线程:使用pthread_detach()函数。(作用:如果线程数量较多,且主线程不需要等待每个线程结束,将线程设置为分离状态可以避免资源泄漏。

int pthread_detach(pthread_t thread);

pthread_t thread:需要设置为分离状态的线程的 ID。这个 ID 是通过 pthread_create()                                      创建线程时返回的。

返回值

                                  如果操作成功,pthread_detach() 返回 0。如果失败,返回一个错误                                      码(非零值)

关闭线程:使用close()函数。(同上) 

三、与管道相关的函数 

创建管道:使用pipe()函数。

int pipe(int pipefds[2]);

pipefds:一个整型数组,用于存储管道的两个文件描述符(file descriptors)。pipefds[0]                  是管道的读端,pipefds[1] 是管道的写端。

返回值

                如果成功,返回 0,如果失败,返回 -1

关闭管道:使用close()函数。(同上)

四、服务端代码实现 

        服务端对于只有单个客户端的socket通信。

用到的结构体sockaddr_in:

struct sockaddr_in {
    uint8_t sin_len;          // 地址结构长度(可选,具体依赖于平台)
    sa_family_t sin_family;   // 地址族,必须是 AF_INET
    uint16_t sin_port;        // 网络字节序的端口号
    struct in_addr sin_addr;  // IPv4 地址
    char sin_zero[8];         // 填充字段,用于对齐
};

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>         // POSIX 系统调用库,用于 close 等函数
#include <arpa/inet.h>      // 网络地址操作库,用于 inet_ntoa 等函数

int main() {
    int server_fd, client_fd;          // 服务器和客户端的文件描述符
    struct sockaddr_in server_addr, client_addr;  // 服务器和客户端的地址结构
    socklen_t addr_len = sizeof(client_addr);     // 客户端地址结构的长度
    char buffer[1024];                            // 数据缓冲区

    // 创建Socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);  // 创建一个 TCP 套接字
    if (server_fd == -1) {                        // 如果创建失败
        perror("Socket creation failed");         // 打印错误信息
        exit(EXIT_FAILURE);                        // 退出程序
    }

    // 配置服务器地址和端口
    memset(&server_addr, 0, sizeof(server_addr)); // 将服务器地址结构清零
    server_addr.sin_family = AF_INET;             // 设置地址族为 IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;     // 监听所有网络接口
    server_addr.sin_port = htons(8080);          // 设置监听端口为 12345(网络字节序)

    // 绑定Socket
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");                    // 如果绑定失败
        close(server_fd);                          // 关闭套接字
        exit(EXIT_FAILURE);                        // 退出程序
    }

    // 开始监听
    if (listen(server_fd, 5) == -1) {             // 开始监听,允许的最大连接队列为 5
        perror("Listen failed");                  // 如果监听失败
        close(server_fd);                          // 关闭套接字
        exit(EXIT_FAILURE);                        // 退出程序
    }

    printf("Server is listening on port 8080...\n");  // 打印服务器正在监听的信息

    // 接受客户端连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len); // 接受客户端连接
    if (client_fd == -1) {                              // 如果接受失败
        perror("Accept failed");                        // 打印错误信息
        close(server_fd);                                // 关闭服务器套接字
        exit(EXIT_FAILURE);                              // 退出程序
    }

    // 打印客户端的 IP 和端口
    printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 接收客户端数据
    memset(buffer, 0, sizeof(buffer));  // 清空缓冲区
    if (recv(client_fd, buffer, sizeof(buffer), 0) == -1) { // 接收客户端发送的数据
        perror("Receive failed");                           // 如果接收失败
    } else {
        printf("Received from client: %s\n", buffer);       // 打印接收到的数据
    }

    // 发送响应
    const char* response = "Hello from server";             // 响应消息
    if (send(client_fd, response, strlen(response), 0) == -1) { // 向客户端发送响应
        perror("Send failed");                              // 如果发送失败
    }

    // 关闭连接
    close(client_fd);  // 关闭客户端连接
    close(server_fd);  // 关闭服务器套接字
    return 0;
}

         但我们要相互通信,至少是有两个客户端的,一个是我们的esp32,一个是控制esp32的设备。所以我们需要使用多线程,对上面代码进行修改,增加多线程的实现。

相比上面代码的修改:

//1.定义两个变量用于区分两个不同的客户端(esp32和控制端)。
int A_fd;
int B_fd;


//2.创建多个socket,因为我们有多个客户端,并创建多个线程与多个客户端连接。
while(1){
    //创建socket
	if ((client_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
        perror("Accept failed");
        exit(EXIT_FAILURE);
    	}else{
            //先从socket中读取一个数据,后续在esp32和控制端连接服务端时,发送一个字符用于区分,这里我用“A”表示esp32,“B”或其他字符来表是控制端。
            read(client_socket, buffer, BUFFER_SIZE);
	        if(strcmp(buffer,"A")==0){
    		    A_fd=client_socket;
		        printf("A Client connected.\n");
	        }else{
		        B_fd=client_socket;
		        printf("B Client connected.\n");
            }
	    }
    }
    //创建线程,并将socket标识符作为参数传入线程处理函数(为了后面区分esp32和控制端)
    pthread_t thread_id;
	int* pclient_fd=malloc(sizeof(int));
	*pclient_fd=client_socket;
	if(pthread_create(&thread_id,NULL,handle_client,pclient_fd)!=0){
	    perror("Thread creation failed");
	}else{
	    pthread_detach(thread_id);
	}
}

修改后的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 8080  // 监听端口
#define BUFFER_SIZE 1024  // 缓冲区大小

int A_fd;
int B_fd;

//pthread调用的处理函数
void* handle_client(void* arg){
    int client_fd = *(int*)arg;
    free(arg);
    char buffer[BUFFER_SIZE];
    if(client_fd==A_fd){
    	while(1){
        //.....
        //对esp32的数据处理
	    }
        close(pipefds[0]);
	}else{
    	while(1){
        //.....
        //对控制端的数据处理
    	}
        close(pipefds[1]);
    }

    close(client_fd);
    return NULL;
}

int main() {
    int server_fd, client_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    if(pipe(pipefds)==-1){
    	perror("pippe error");
	exit(EXIT_FAILURE);
    }

    // 创建Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // 本地所有IP地址
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    while(1){
    //创建socket
	if ((client_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
        perror("Accept failed");
        exit(EXIT_FAILURE);
    	}else{
            //先从socket中读取一个数据,后续在esp32和控制端连接服务端时,发送一个字符用于区分,这里我用“A”表示esp32,“B”或其他字符来表是控制端。
	        read(client_socket, buffer, BUFFER_SIZE);
    	    printf("Message from client: %s\n", buffer);
	    if(strcmp(buffer,"A")==0){
    		A_fd=client_socket;
		    printf("A Client connected.\n");
	    }else{
		    B_fd=client_socket;
		    printf("B Client connected.\n");
	    }
	}

    //创建线程,并将socket标识符作为参数传入线程处理函数(为了后面区分esp32和控制端)
	pthread_t thread_id;
	int* pclient_fd=malloc(sizeof(int));
	*pclient_fd=client_socket;
	if(pthread_create(&thread_id,NULL,handle_client,pclient_fd)!=0){
	    perror("Thread creation failed");
	}else{
	    pthread_detach(thread_id);
	}
    }

    // 关闭连接
    close(server_fd);

    return 0;
}

        继续完成数据传输部分,这里我只处理从控制端到esp32的数据,即数据只从控制端发往esp32,想实现消息传输可以使用全局变量,因为pthread创建的子线程是与主程序内存共享的,但要使用锁,以避免同时修改同一个变量导致的数据割裂的情况,但我这里图方便使用管道实现两个子线程间的数据传输。

最后完整的服务端代码及注释:

#include <stdio.h>
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h>       // POSIX 系统调用
#include <arpa/inet.h>    // 网络编程
#include <pthread.h>      // 多线程支持

#define PORT 8080          // 监听端口
#define BUFFER_SIZE 1024   // 缓冲区大小

int A_fd;                 // 客户端 A 的文件描述符
int B_fd;                 // 客户端 B 的文件描述符
int pipefds[2];           // 管道文件描述符,用于中转数据

// 处理客户端的线程函数
void* handle_client(void* arg) {
    int client_fd = *(int*)arg;  // 获取客户端的文件描述符
    free(arg);                   // 释放传递的动态分配的内存
    char buffer[BUFFER_SIZE];    // 数据缓冲区

    // 处理客户端 esp32
    if (client_fd == A_fd) {
        while (1) {
            memset(buffer, 0, BUFFER_SIZE);  // 清空缓冲区
            // 从管道读取数据
            read(pipefds[0], buffer, sizeof(buffer) - 1);
            // 将数据发送给客户端 A
            send(client_fd, buffer, strlen(buffer), 0);
            // 如果客户端 A 发送 "q",则退出
            if (strcmp(buffer, "q") == 0) {
                printf("%s Closed\n", "A client");
                close(pipefds[0]);  // 关闭管道读端
                break;
            }
        }
    }
    // 处理客户端 控制端
    else {
        while (1) {
            memset(buffer, 0, BUFFER_SIZE);  // 清空缓冲区
            // 接收客户端发送的数据
            int bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
            // 将数据写入管道
            write(pipefds[1], buffer, strlen(buffer));
            // 如果客户端断开连接或发送失败
            if (bytes_received <= 0) {
                printf("%s Closed\n", "B client");
                close(pipefds[1]);  // 关闭管道写端
                break;
            }
            // 向客户端 B 发送确认消息
            send(client_fd, "OK", strlen("OK"), 0);
        }
    }

    // 关闭客户端连接
    close(client_fd);
    return NULL;
}

int main() {
    int server_fd, client_socket;  // 服务器和客户端的文件描述符
    struct sockaddr_in address;   // 服务器地址结构
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建管道
    if (pipe(pipefds) == -1) {
        perror("pipe error");
        exit(EXIT_FAILURE);
    }

    // 创建 Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口
    address.sin_port = htons(PORT);        // 设置监听端口

    // 绑定 Socket
    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    while (1) {
        // 接受客户端连接
        if ((client_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
            perror("Accept failed");
            exit(EXIT_FAILURE);
        } else {
            // 读取客户端发送的标识信息
            read(client_socket, buffer, BUFFER_SIZE);
            printf("Message from client: %s\n", buffer);
            // 根据标识信息判断是客户端 A 还是 B
            if (strcmp(buffer, "A") == 0) {
                A_fd = client_socket;
                printf("A Client connected.\n");
            } else {
                B_fd = client_socket;
                printf("B Client connected.\n");
            }
        }

        // 创建线程处理客户端
        pthread_t thread_id;
        int* pclient_fd = malloc(sizeof(int));  // 动态分配内存以传递文件描述符
        *pclient_fd = client_socket;
        if (pthread_create(&thread_id, NULL, handle_client, pclient_fd) != 0) {
            perror("Thread creation failed");
        } else {
            pthread_detach(thread_id);  // 分离线程,使其在完成后自动回收资源
        }
    }

    // 关闭服务器 Socket
    close(server_fd);

    return 0;
}

 客户端

esp32端 

#include <Arduino.h>
#include <WiFi.h>

WiFiClient wifiClient;  // 创建一个 WiFi 客户端对象
String SSID = "";       // WiFi 网络的 SSID(名称)
String password = "";   // WiFi 网络的密码
char *serverIp = "";    // 公网服务器的 IP 地址,如果是局域网部署就用内网地址
uint16_t serverPort = 8080;  // 服务器的端口号
int wifiConnect(String, String);  // 声明 WiFi 连接函数

void setup() {
  Serial.begin(9600);  // 初始化串口通信,波特率为 9600
  pinMode(2, OUTPUT);  // 设置数字引脚 2 为输出模式(用于指示灯)

  // 尝试连接 WiFi
  if (wifiConnect(SSID, password)) {
    Serial.println("WiFi Connected");  // 如果连接成功,打印连接成功信息
    // 尝试连接到服务器
    if (wifiClient.connect(serverIp, serverPort)) {
      Serial.println("server Connected");  // 如果连接到服务器成功
      wifiClient.println("A");  // 向服务器发送标识符 "A"
      digitalWrite(2, HIGH);    // 点亮指示灯
    } else {
      Serial.println("server Disconnected");  // 如果连接到服务器失败
      while (1);  // 进入死循环,程序停止
    }
  } else {
    while (1);  // 如果 WiFi 连接失败,进入死循环,程序停止
  }
}

void loop() {
  // 检查是否仍然连接到服务器
  if (wifiClient.connected()) {
    delay(1000);  // 每秒检查一次
    // 检查是否有数据从服务器可用
    while (wifiClient.available()) {
      char c = wifiClient.read();  // 读取一个字符
      Serial.println(c);  // 打印到串口监视器
    }
  } else {
    Serial.println("Connection lost. Reconnecting...");  // 如果连接丢失
    digitalWrite(2, LOW);  // 熄灭指示灯
    // 尝试重新连接到服务器
    if (wifiClient.connect(serverIp, serverPort)) {
      wifiClient.println("A");  // 重新连接后发送标识符 "A"
      Serial.println("Reconnected to server");  // 打印重新连接成功信息
      digitalWrite(2, HIGH);  // 点亮指示灯
    }
  }
}

// WiFi 连接函数
int wifiConnect(String SSID, String password) {
  WiFi.begin(SSID, password);  // 开始连接到指定的 WiFi 网络
  Serial.printf("WiFiConnect");  // 打印连接信息
  for (int i = 0; i <= 4; i++) {
    sleep(1);  // 等待 1 秒
    Serial.printf(".");  // 打印一个点,表示正在尝试连接
    if (WiFi.status() == WL_CONNECTED)  // 检查是否连接成功
      return 1;  // 如果连接成功,返回 1
  }
  Serial.println();  // 打印换行
  return 0;  // 如果连接失败,返回 0
}

然后使用串口调试器查看

控制端

        我没有写控制端程序,可以使用一般的socket通信工具测试,也可以使用电脑自带的telnet工具测试,在控制台下使用

telnet IP Prot

进行测试,先谁便输一个字符,用作上面在服务端提到的标识符以区别esp32与控制端

 总结

        .....(假装有内容)

        最后祝大家学习进步、事业有成、生活美满。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值