ESP-C3入门9. 创建TCP Server
- 一、ESP32 IDF的TCP/IP协议栈
- 二、BSD套接字API介绍
- 三、创建TCP Server的步骤
- 1. 引用TCP/IP协议栈
- 2. 创建 TCP套接字拼绑定端口
- 3. 接收客户端请求
- 4. 启动服务
- 四、完整代码
- 1. wifi.h
- 2. wifi.c
- 3. tcpServer.h
- 4. tcpServer.c
- 5. main.c
- 6. CmakeLists.txt
- 7. 组件内CMakeLists.txt内容
一、ESP32 IDF的TCP/IP协议栈
TCP/IP协议栈是ESP32 IDF的一个核心组件。它实现了TCP、UDP、IP、DHCP、DNS和其他网络协议,使ESP32可以与其他设备通信。具体来说,ESP32 IDF的TCP/IP协议栈包括以下几个主要模块:
- WiFi协议栈
- TCP/IP协议栈
- LWIP协议栈
- SPI Flash文件系统
通过TCP/IP协议栈,ESP32可以轻松地实现各种网络应用,例如HTTP服务器、MQTT客户端、TCP/UDP服务器和客户端等等。同时,ESP32 IDF提供了丰富的API和组件,使得开发人员可以快速地实现自己的应用程序。
ESP32主要使用LwIP协议栈,支持函数:
- BSD风格 Sockets API
- Netconn API (未正式启用)
官方文档提示:
在使用任何lwIP API(除BSD Sockets API之外)时,请确保它是线程安全的。
要检查特定的API调用是否安全,请启用CONFIG_LWIP_CHECK_THREAD_SAFETY并运行应用程序。这样,lwIP会断言TCP/IP核心功能被正确访问,如果没有正确锁定或从正确的任务(lwIP FreeRTOS任务)访问,则执行中止。
一般建议使用ESP-NETIF组件与lwIP交互。
ESP-IDF对lwIP进行了一些封装,以间接支持部分功能,如:
- DHCP
- SNTP
- ICMP Ping
- NetBIOS查找可使用的lwIP API
- mDNS
- 串行PPP接口
等。
二、BSD套接字API介绍
BSD Sockets API 是一个常见的跨平台TCP/IP套接字API, 有时被称为POSIX Sockets或 Berkeley Sockets。
lwIP支持BSD Sockets API的所有常见用法,一些功能如下:
- socket()
- bind()
- accept()
- shutdown()
- getpeername()
- getsockopt()
- setsockopt()
- close()
- read(), readv() write() writev()
- recv() recvmsg() recfrom()
- send() sendmsg() sendto()
- select()
- poll()
- fcntl()
另外lwIP还支持ioctl()非标准的功能。
三、创建TCP Server的步骤
1. 引用TCP/IP协议栈
在CMakeLists.txt包含以下内容:
idf_component_register(SRCS "your_source_files.c"
INCLUDE_DIRS "include"
REQUIRES "tcpip_adapter")
2. 创建 TCP套接字拼绑定端口
创建一个TCP套接字,并将其绑定到指定的端口。然后使用listen()函数将服务器套接字设置为侦听连接。
#include <string.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netdb.h>
#include <lwip/sockets.h>
#define PORT CONFIG_EXAMPLE_PORT
static struct sockaddr_in server_addr;
static int server_socket = -1;
void create_tcp_server() {
server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (server_socket < 0) {
ESP_LOGE(TAG, "Failed to create server socket!");
return;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
int err = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (err != 0) {
ESP_LOGE(TAG, "Failed to bind server socket!");
close(server_socket);
return;
}
err = listen(server_socket, 1);
if (err != 0) {
ESP_LOGE(TAG, "Failed to listen on server socket!");
close(server_socket);
return;
}
ESP_LOGI(TAG, "TCP server started on port %d", PORT);
}
3. 接收客户端请求
使用accept()函数从服务器套接字接受新连接。
下面代码里,创建一个TCP客户端,使用结构体client_addr存储客户端的信息。
- 接收到一般消息时,自动原文回复;
- 收到ping时,回复pong;
- 收到bye时,回复bye并关闭连接。
关闭连接后,程序会重新开始等待新连接。
void process_tcp_client(){
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if(client_socket <0){
ESP_LOGE(TAG, "Failed to accept client socket!");
return;
}
ESP_LOGI(TAG, "Accepted new client connection");
// 处理客户端请求
bool end=false;
while (!end) {
char buffer[1024];
// 初始化数组
memset(buffer, 0, sizeof(buffer));
int len = recv(client_socket, buffer, sizeof(buffer), 0);
if (len > 0) {
const char *response;
if (strncmp(buffer, "ping", 4) == 0) {
response = "pong";
ESP_LOGI(TAG, "Received ping, sending pong");
} else if (strncmp(buffer, "bye", 3) == 0) {
response = "bye";
ESP_LOGI(TAG, "Received bye, closing connection");
end = true;
} else {
response = buffer;
ESP_LOGI(TAG, "Received data: %s", response);
}
send(client_socket, response, strlen(response), 0);
}
}
close(client_socket);
}
4. 启动服务
在应用程序的main()函数中调用create_tcp_server()函数来启动TCP服务器
void app_main() {
create_tcp_server();
while (1) {
process_tcp_client();
}
}
四、完整代码
本文代码会基于前一章内容实现,将wifi连接部分独立文件放。
项目框架:
1. wifi.h
#ifndef WIFI_LIB
#define WIFI_LIB
#include <string.h>
/* 宏定义 */
#include <esp_event_base.h>
#include "esp_log.h"
#define LIGHT_ESP_WIFI_SSID "你的wifi账号"
#define LIGHT_ESP_WIFI_PASS "你的wifi密码"
#define LIGHT_ESP_MAXIMUM_RETRY 5
// 事件组允许每个事件有多个位,这里只使用两个事件:
// 已经连接到AP并获得了IP
// 在最大重试次数后仍未连接
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
/* 函数声明 */
void wifi_initialize(void);
void wifi_station_initialize(void);
void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
#endif /* WIFI_LIB */
2. wifi.c
#include <freertos/FreeRTOS.h>
#include "main/network/include/wifi.h"
#include <freertos/event_groups.h>
#include <esp_wifi.h>
static const char *TAG = "wifi lib";
// FreeRTOS事件组,连接成功时发出信号
static EventGroupHandle_t s_wifi_event_group = NULL;
static int s_retry_num = 0;
// 事件回调
void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
// 如果是Wi-Fi事件,并且事件ID是Wi-Fi事件STA_START
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) {
// 如果是Wi-Fi事件,并且事件ID是Wi-Fi事件STA_DISCONNECTED
/* 如果重试次数小于最大重试次数 */
if (s_retry_num < LIGHT_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事件,并且事件ID是IP事件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));
// 将重试次数重置为 0;
s_retry_num = 0;
// 通过调用 xEventGroupSetBits 函数,将 WIFI_CONNECTED_BIT 设置到事件组中,表示成功连接到 AP
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_initialize(void)
{
// 创建一个事件组,用于管理Wi-Fi连接事件。
s_wifi_event_group = xEventGroupCreate();
// 初始化 TCP/IP 协议栈。
ESP_ERROR_CHECK(esp_netif_init());
// 创建默认事件循环。
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 创建默认的Wi-Fi网络接口。
esp_netif_create_default_wifi_sta();
// 设置 Wi-Fi 初始化配置为默认配置
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册事件处理器,以处理 Wi-Fi 和 IP 相关事件
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
}
void wifi_station_initialize(void)
{
// 配置WiFi的配置信息
wifi_config_t wifi_config = {
.sta = {
.ssid = LIGHT_ESP_WIFI_SSID,
.password = LIGHT_ESP_WIFI_PASS,
// 启用WPA2模式,常用的WiFi连接方式
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
},
};
// WiFi工作模式设置为STA
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
// 设置WiFi工作模式
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// 启动WiFi
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_station_initialize finished.");
/* 等待连接建立(WIFI_CONNECTED_BIT)或连接失败的次数达到最大值(WIFI_FAIL_BIT)。
* 这些位通过 event_handler() 设置(详见上面)*/
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
/* xEventGroupWaitBits() 返回调用前的 bits,因此我们可以测试实际发生了什么事件。 */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", LIGHT_ESP_WIFI_SSID, LIGHT_ESP_WIFI_PASS);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", LIGHT_ESP_WIFI_SSID, LIGHT_ESP_WIFI_PASS);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
}
3. tcpServer.h
#ifndef TCP_SERVER
#define TCP_SERVER
#include <string.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netdb.h>
#include <lwip/sockets.h>
#define PORT 3000
void create_tcp_server();
void process_tcp_client();
#endif
4. tcpServer.c
//
//
//
#include <esp_log.h>
#include "tcpServer.h"
static struct sockaddr_in server_addr;
static int server_socket = -1;
static const char* TAG = "TCP SERVER";
void create_tcp_server(){
server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if(server_socket<0){
ESP_LOGE(TAG, "Failed to create server socket!");
return;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
int err = bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(err !=0){
ESP_LOGE(TAG, "Failed to bind server socket!");
close(server_socket);
return;
}
err = listen(server_socket, 1);
if(err != 0){
ESP_LOGE(TAG, "Failed to listen on server socket!");
close(server_socket);
return;
}
ESP_LOGI(TAG, "TCP server started on port %d", PORT);
}
void process_tcp_client(){
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if(client_socket <0){
ESP_LOGE(TAG, "Failed to accept client socket!");
return;
}
ESP_LOGI(TAG, "Accepted new client connection");
// 处理客户端请求
bool end=false;
while (!end) {
char buffer[1024];
// 初始化数组
memset(buffer, 0, sizeof(buffer));
int len = recv(client_socket, buffer, sizeof(buffer), 0);
if (len > 0) {
const char *response;
if (strncmp(buffer, "ping", 4) == 0) {
response = "pong";
ESP_LOGI(TAG, "Received ping, sending pong");
} else if (strncmp(buffer, "bye", 3) == 0) {
response = "bye";
ESP_LOGI(TAG, "Received bye, closing connection");
end = true;
} else {
response = buffer;
ESP_LOGI(TAG, "Received data: %s", response);
}
send(client_socket, response, strlen(response), 0);
}
}
close(client_socket);
}
5. main.c
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <nvs_flash.h>
#include "network/include/wifi.h"
#include "network/include/tcpServer.h"
static const char *TAG = "wifi connection";
void app_main()
{
int i = 0;
ESP_LOGE(TAG, "app_main");
// 初始化NVS存储区
ESP_ERROR_CHECK(nvs_flash_init());
// Wi-Fi初始化
ESP_LOGI(TAG, "Wi-Fi initialization");
wifi_initialize();
// Wi-Fi Station初始化
wifi_station_initialize();
// 创建 tcp server
create_tcp_server();
while (1) {
ESP_LOGI(TAG, "[%02d] Wait Client Connection!", i++);
process_tcp_client();
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
6. CmakeLists.txt
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(PYTHON "D:/Espressif/python_env/idf4.4_py3.8_env/Scripts/python")
# 设置项目的额外组件目录,允许使用ESP-IDF示例中的公共组件。
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/led_strip)
# 设置项目的目标芯片。这决定了编译器和链接器的选项。
set(IDF_TARGET "esp32c3")
# 设置项目使用的C标准
set(CMAKE_C_STANDARD 99)
# 设置项目的源文件,即要编译和链接的文件
set(SOURCES
${SOURCES}
)
# 设置项目的包含目录,这些是要搜索头文件的目录
set(INCLUDE_DIRS
${INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}
)
include_directories(${INCLUDE_DIRS})
# 包含ESP-IDF工具中的project.cmake文件。这设置了项目的配置和构建系统。
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# 设置项目的名称
project(blink)
7. 组件内CMakeLists.txt内容
idf_component_register(SRCS "main.c" "network/wifi.c" "network/tcpServer.c"
INCLUDE_DIRS "network/include"
REQUIRES "tcpip_adapter" "nvs_flash"
)
# 这将把当前组件的路径添加到编译器的头文件搜索路径中,否则提示找不到string.h等头文件
target_include_directories(${COMPONENT_LIB} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_include_directories(${COMPONENT_LIB} PUBLIC "network/include")