ESP32作为物联网网关,除了采集传感器数据,最重要的任务就是把数据安全地传输到云端。在我们沧州虎王科技技术团队的多个项目中,ESP32需要通过HTTP协议与后端服务器通信,调用RESTful API上报数据、接收指令、下载配置。而一旦涉及HTTPS,TLS加密通信就成了绕不过去的技术门槛。
本文从实战角度出发,分享我们在ESP32上实现HTTP客户端的完整方案,涵盖TLS证书配置、RESTful API封装、错误重试机制、连接池管理等工程细节。
一、ESP32的HTTP/TLS技术栈
ESP-IDF提供了 esp_http_client 组件,底层基于mbedTLS实现TLS加密。整体技术栈如下:
┌─────────────────────────────────────────┐
│ 应用层 (RESTful API封装) │
│ http_post("/api/telemetry", body) │
├─────────────────────────────────────────┤
│ esp_http_client (ESP-IDF组件) │
│ - HTTP请求/响应 - 重定向 - 流式传输 │
├─────────────────────────────────────────┤
│ mbedTLS (TLS/SSL引擎) │
│ - TLS握手 - 证书验证 - 加密解密 │
├─────────────────────────────────────────┤
│ lwIP (TCP/IP协议栈) │
│ - TCP连接 - DNS解析 - Socket管理 │
├─────────────────────────────────────────┤
│ WiFi (esp_wifi) / Ethernet │
└─────────────────────────────────────────┘
理解这个层次结构很重要,因为TLS问题往往出在mbedTLS层,网络超时往往出在lwIP层,HTTP解析问题出在esp_http_client层。调试时需要逐层定位。
二、WiFi连接管理:HTTP通信的前置条件
在做HTTP通信之前,首先要确保WiFi连接稳定。我们封装了一个WiFi连接管理器,处理连接、断线重连、事件通知:
c
// wifi_manager.c
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static EventGroupHandle_t s_wifi_event_group;
static int s_retry_count = 0;
static bool s_wifi_connected = false;
static void wifi_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) {
s_wifi_connected = false;
if (s_retry_count < 5) {
esp_wifi_connect();
s_retry_count++;
ESP_LOGW(TAG, "WiFi retry %d/5", s_retry_count);
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
} 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_count = 0;
s_wifi_connected = true;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
esp_err_t wifi_init(const char *ssid, const char *password) {
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_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, &instance_any_id);
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, &instance_got_ip);
wifi_config_t wifi_config = {0};
strcpy((char *)wifi_config.sta.ssid, ssid);
strcpy((char *)wifi_config.sta.password, password);
wifi_config.sta.threshold.authmode = WIFI_AUTH_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());
// 等待连接
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
pdMS_TO_TICKS(15000));
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "WiFi connected to %s", ssid);
return ESP_OK;
}
ESP_LOGE(TAG, "WiFi connection failed");
return ESP_FAIL;
}
bool wifi_is_connected(void) {
return s_wifi_connected;
}
三、TLS证书配置:HTTPS的核心
ESP32使用mbedTLS实现TLS,需要配置CA证书用于验证服务器证书。有两种方式:
方式一:嵌入CA证书(适合证书固定的场景)
从浏览器导出服务器CA证书的PEM格式,嵌入到固件中:
c
// ca_cert.h
static const char server_cert_pem[] =
"-----BEGIN CERTIFICATE-----\n"
"MIIDXTCCAkWgAwIBAgIJAKDxYJ5k5g3cMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n"
// ... 完整证书内容 ...
"-----END CERTIFICATE-----\n";
方式二:使用ESP-IDF内置的根证书包
ESP-IDF支持通过 menuconfig 启用mbedTLS根证书包,内置了常见的CA根证书:
Component config → mbedTLS → Certificate Bundle → Enable Mozilla root CA bundle
我们沧州虎王科技的物联网平台使用Let's Encrypt证书,所以选择方式二更方便。但如果是内网部署的自签名证书,则必须用方式一。
以下是HTTP客户端的TLS配置代码:
c
// http_client.c
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#define MAX_HTTP_OUTPUT_BUFFER 4096
typedef struct {
char *response_buf;
int response_len;
int response_capacity;
int status_code;
} http_response_t;
static esp_err_t http_event_handler(esp_http_client_event_t *evt) {
http_response_t *resp = (http_response_t *)evt->user_data;
switch (evt->event_id) {
case HTTP_EVENT_ON_DATA:
if (resp->response_len + evt->data_len < resp->response_capacity) {
memcpy(resp->response_buf + resp->response_len, evt->data, evt->data_len);
resp->response_len += evt->data_len;
} else {
ESP_LOGW(TAG, "Response buffer overflow, truncating");
}
break;
case HTTP_EVENT_ON_FINISH:
resp->response_buf[resp->response_len] = '\0';
break;
case HTTP_EVENT_ERROR:
ESP_LOGE(TAG, "HTTP error: %s", esp_err_to_name(evt->error_handle->esp_error_code));
break;
default:
break;
}
return ESP_OK;
}
// 创建配置好TLS的HTTP客户端
esp_http_client_handle_t http_client_create(const char *url) {
esp_http_client_config_t config = {
.url = url,
.event_handler = http_event_handler,
.cert_pem = NULL, // 使用证书包而非单独证书
.use_global_ca_store = false,
.crt_bundle_attach = esp_crt_bundle_attach, // 启用内置根证书包
.timeout_ms = 10000, // 10秒超时
.buffer_size = 1024, // 接收缓冲区
.buffer_size_tx = 1024, // 发送缓冲区
.disable_auto_redirect = false,
.max_redirection_count = 3,
};
return esp_http_client_init(&config);
}
四、RESTful API封装
基于 esp_http_client,我们封装了一套RESTful API客户端,支持GET/POST/PUT/DELETE,自动处理JSON序列化/反序列化:
c
// rest_api.h
typedef struct {
int status_code;
char *body;
int body_len;
} api_response_t;
typedef enum {
API_OK = 0,
API_ERR_NETWORK, // 网络错误
API_ERR_TIMEOUT, // 超时
API_ERR_TLS, // TLS错误
API_ERR_HTTP, // HTTP错误(非2xx状态码)
API_ERR_JSON, // JSON解析错误
API_ERR_MEMORY, // 内存不足
} api_error_t;
// API配置
typedef struct {
char base_url[128]; // 如 "https://iot.cztech.cn/api/v1"
char auth_token[128]; // Bearer Token
char device_id[64]; // 设备ID
uint8_t max_retries; // 最大重试次数
uint32_t timeout_ms; // 超时时间
} api_config_t;
static api_config_t s_api_config;
static const char *TAG_API = "REST_API";
void api_init(const api_config_t *config) {
memcpy(&s_api_config, config, sizeof(api_config_t));
}
// 发送HTTP请求
static api_error_t api_send_request(const char *method_str,
const char *path,
const char *body,
int body_len,
const char *content_type,
api_response_t *response) {
if (!wifi_is_connected()) {
ESP_LOGW(TAG_API, "WiFi not connected, cannot send request");
return API_ERR_NETWORK;
}
// 构建完整URL
char url[256];
snprintf(url, sizeof(url), "%s%s", s_api_config.base_url, path);
http_response_t http_resp = {0};
http_resp.response_buf = malloc(MAX_HTTP_OUTPUT_BUFFER);
if (!http_resp.response_buf) return API_ERR_MEMORY;
http_resp.response_capacity = MAX_HTTP_OUTPUT_BUFFER;
esp_http_client_config_t config = {
.url = url,
.event_handler = http_event_handler,
.user_data = &http_resp,
.crt_bundle_attach = esp_crt_bundle_attach,
.timeout_ms = s_api_config.timeout_ms,
.buffer_size = 2048,
.buffer_size_tx = 2048,
.is_async = false,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// 设置方法
if (strcmp(method_str, "POST") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_POST);
} else if (strcmp(method_str, "PUT") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_PUT);
} else if (strcmp(method_str, "DELETE") == 0) {
esp_http_client_set_method(client, HTTP_METHOD_DELETE);
}
// 设置认证头
char auth_header[256];
snprintf(auth_header, sizeof(auth_header), "Bearer %s", s_api_config.auth_token);
esp_http_client_set_header(client, "Authorization", auth_header);
esp_http_client_set_header(client, "Content-Type", content_type);
esp_http_client_set_header(client, "X-Device-Id", s_api_config.device_id);
esp_http_client_set_header(client, "User-Agent", "CZTech-ESP32/1.0");
// 设置请求体
if (body && body_len > 0) {
esp_http_client_set_post_field(client, body, body_len);
}
// 发送请求
esp_err_t err = esp_http_client_perform(client);
api_error_t result = API_OK;
if (err == ESP_OK) {
response->status_code = esp_http_client_get_status_code(client);
response->body = http_resp.response_buf;
response->body_len = http_resp.response_len;
if (response->status_code >= 200 && response->status_code < 300) {
ESP_LOGD(TAG_API, "%s %s -> %d (%d bytes)", method_str, path,
response->status_code, response->body_len);
} else {
ESP_LOGW(TAG_API, "%s %s -> %d: %.*s", method_str, path,
response->status_code, response->body_len, response->body);
result = API_ERR_HTTP;
}
} else {
if (err == ESP_ERR_HTTP_CONNECT) result = API_ERR_NETWORK;
else if (err == ESP_ERR_HTTP_FETCH_HEADER || err == ESP_ERR_HTTP_INVALID_TRANSPORT) {
result = API_ERR_TLS;
} else {
result = API_ERR_NETWORK;
}
ESP_LOGE(TAG_API, "HTTP request failed: %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
if (result != API_OK) {
free(http_resp.response_buf);
response->body = NULL;
}
return result;
}
// 带重试的请求发送
static api_error_t api_send_with_retry(const char *method,
const char *path,
const char *body,
int body_len,
const char *content_type,
api_response_t *response) {
api_error_t err;
int retry_delay_ms = 1000;
for (int i = 0; i <= s_api_config.max_retries; i++) {
err = api_send_request(method, path, body, body_len, content_type, response);
if (err == API_OK) return err;
// 只对网络/超时错误重试,不对HTTP 4xx错误重试
if (err == API_ERR_HTTP) return err;
if (i < s_api_config.max_retries) {
ESP_LOGW(TAG_API, "Retry %d/%d after %dms (error=%d)",
i + 1, s_api_config.max_retries, retry_delay_ms, err);
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
retry_delay_ms = (retry_delay_ms * 2 < 16000) ? retry_delay_ms * 2 : 16000;
}
}
return err;
}
基于这个底层封装,实现具体的RESTful API调用就非常简洁了:
c
// 上报遥测数据
api_error_t api_post_telemetry(const char *json_payload) {
api_response_t resp = {0};
return api_send_with_retry("POST", "/telemetry",
json_payload, strlen(json_payload),
"application/json", &resp);
}
// 获取设备配置
api_error_t api_get_config(cJSON **config_json) {
api_response_t resp = {0};
api_error_t err = api_send_with_retry("GET", "/config",
NULL, 0, NULL, &resp);
if (err == API_OK) {
*config_json = cJSON_Parse(resp.body);
if (!*config_json) err = API_ERR_JSON;
free(resp.body);
}
return err;
}
// 上报设备状态
api_error_t api_put_status(const char *status_json) {
api_response_t resp = {0};
return api_send_with_retry("PUT", "/status",
status_json, strlen(status_json),
"application/json", &resp);
}
// 下载固件更新包
api_error_t api_download_ota(const char *version,
esp_http_client_handle_t *client) {
char path[128];
snprintf(path, sizeof(path), "/ota/%s", version);
// OTA下载需要流式处理,返回client句柄由调用方逐步读取
// ... 实现省略 ...
return API_OK;
}
五、MQTT与HTTP的混合使用策略
在实际项目中,HTTP和MQTT各有适用场景。我们的策略是:
- HTTP:用于设备注册、配置拉取、OTA升级、用户指令下发等低频、请求-响应式通信
- MQTT:用于遥测数据实时上报、告警事件推送等高频、发布-订阅式通信
c
// 混合通信策略示例
void data_report_task(void *arg) {
sensor_value_t values[8];
int count;
int report_counter = 0;
while (1) {
// 读取传感器数据
device_read("temp_01", values, 8, &count);
// 构建JSON
cJSON *root = cJSON_CreateObject();
cJSON *props = cJSON_CreateObject();
for (int i = 0; i < count; i++) {
switch (values[i].type) {
case SENSOR_TYPE_TEMP:
cJSON_AddNumberToObject(props, "temperature", values[i].value.temperature);
break;
case SENSOR_TYPE_HUMIDITY:
cJSON_AddNumberToObject(props, "humidity", values[i].value.humidity);
break;
}
}
cJSON_AddItemToObject(root, "properties", props);
cJSON_AddNumberToObject(root, "timestamp", esp_timer_get_time() / 1000);
char *json_str = cJSON_PrintUnformatted(root);
// 高频遥测走MQTT
mqtt_publish("device/temp_01/telemetry", json_str, 0);
// 每60次(约5分钟)通过HTTP上报一次完整数据快照
if (++report_counter >= 60) {
api_post_telemetry(json_str);
report_counter = 0;
}
free(json_str);
cJSON_Delete(root);
vTaskDelay(pdMS_TO_TICKS(5000)); // 5秒间隔
}
}
六、错误处理与诊断
网络通信中最难处理的不是成功路径,而是各种失败场景。我们建立了一套完整的错误诊断体系:
c
// 错误诊断信息
typedef struct {
api_error_t error;
const char *description;
const char *suggestion;
} error_diag_t;
static const error_diag_t s_error_diags[] = {
{API_OK, "成功", "无需操作"},
{API_ERR_NETWORK, "网络连接失败", "检查WiFi连接、DNS解析、服务器可达性"},
{API_ERR_TIMEOUT, "请求超时", "检查网络延迟、增大超时时间、检查服务器负载"},
{API_ERR_TLS, "TLS握手失败", "检查CA证书有效性、服务器证书是否过期、ESP32时间是否正确"},
{API_ERR_HTTP, "HTTP错误响应", "检查API路径、认证Token、请求体格式"},
{API_ERR_JSON, "JSON解析失败", "检查响应体是否为合法JSON格式"},
{API_ERR_MEMORY, "内存不足", "减少缓冲区大小、检查ESP32内存使用情况"},
};
void api_log_error(api_error_t err) {
for (int i = 0; i < sizeof(s_error_diags) / sizeof(s_error_diags[0]); i++) {
if (s_error_diags[i].error == err) {
ESP_LOGE(TAG_API, "[%d] %s | 建议: %s", err,
s_error_diags[i].description, s_error_diags[i].suggestion);
return;
}
}
}
TLS证书验证失败的排查:这是最常见也最难定位的问题。原因是ESP32的RTC时钟不准,导致证书有效期验证失败。解决方法是在WiFi连接后立即同步NTP时间:
c
// NTP时间同步(TLS证书验证的前提)
void ntp_sync_time(void) {
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "ntp.aliyun.com");
esp_sntp_setservername(1, "pool.ntp.org");
esp_sntp_init();
// 等待同步
time_t now = 0;
struct tm timeinfo = {0};
int retry = 0;
do {
vTaskDelay(pdMS_TO_TICKS(2000));
time(&now);
localtime_r(&now, &timeinfo);
retry++;
} while (timeinfo.tm_year < (2020 - 1900) && retry < 10);
if (timeinfo.tm_year >= (2020 - 1900)) {
ESP_LOGI(TAG, "NTP sync: %ld", (long)now);
} else {
ESP_LOGW(TAG, "NTP sync failed, TLS may not work properly");
}
}
七、内存管理与性能优化
ESP32的可用RAM有限(约320KB SRAM),HTTP+TLS的内存消耗不小。以下是我们的优化经验:
优化一:减小mbedTLS内存占用
在menuconfig中调整mbedTLS配置,减少不必要的加密算法支持:
Component config → mbedTLS:
- 取消不用的加密算法(如Camellia, DES等)
- Maximum SSL fragment size: 4096 → 2048
- Dynamic fragment size: 启用
- Dynamic free peer cert: 启用(握手后释放证书内存)
优化二:HTTP客户端复用
避免每次请求都创建和销毁HTTP客户端。对于频繁请求同一服务器的场景,复用client句柄:
c
static esp_http_client_handle_t s_persistent_client = NULL;
esp_http_client_handle_t api_get_client(void) {
if (s_persistent_client) return s_persistent_client;
esp_http_client_config_t config = {
.url = s_api_config.base_url,
.crt_bundle_attach = esp_crt_bundle_attach,
.timeout_ms = s_api_config.timeout_ms,
.buffer_size = 2048,
};
s_persistent_client = esp_http_client_init(&config);
return s_persistent_client;
}
优化三:响应体按需读取
对于大响应体(如OTA固件下载),不要一次性读入内存,而是分块处理:
c
// OTA分块下载
esp_err_t ota_download(const char *url, esp_ota_handle_t *update_handle) {
esp_http_client_config_t config = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach,
.timeout_ms = 30000,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
return err;
}
int content_length = esp_http_client_fetch_headers(client);
ESP_LOGI(TAG, "OTA image size: %d bytes", content_length);
// 分块读取并写入OTA分区
char *buffer = malloc(4096);
int total_read = 0;
int read_len;
while ((read_len = esp_http_client_read(client, buffer, 4096)) > 0) {
err = esp_ota_write(*update_handle, buffer, read_len);
if (err != ESP_OK) break;
total_read += read_len;
// 打印进度
if (total_read % (4096 * 32) == 0) {
ESP_LOGI(TAG, "OTA progress: %d / %d (%.1f%%)",
total_read, content_length,
(float)total_read / content_length * 100);
}
}
free(buffer);
esp_http_client_close(client);
esp_http_client_cleanup(client);
ESP_LOGI(TAG, "OTA download complete: %d bytes", total_read);
return (total_read == content_length) ? ESP_OK : ESP_FAIL;
}
八、调试工具与实战技巧
在开发HTTP客户端时,调试网络通信是最耗时的环节。我们团队的ESP32工具箱V2.0集成了网络诊断功能,可以快速执行ping测试、DNS查询、HTTP连通性检查,帮助定位是网络层还是应用层的问题。
在现场调试时,随身WiFi硬件调试工具(hardware.czkree.com)的作用更加明显。它可以通过WiFi连接到ESP32的调试串口,手机浏览器上就能查看ESP32的实时日志。我们经常在工厂车间调试时,用手机看日志确认HTTP请求是否成功,不用专门找一台有串口的电脑。
几个实用的调试技巧:
技巧一:抓取HTTP请求详情
在开发阶段,开启详细的日志级别,打印完整的HTTP请求和响应:
c
// 开启详细HTTP日志
esp_log_level_set("HTTP_CLIENT", ESP_LOG_DEBUG);
esp_log_level_set("TRANS_SSL", ESP_LOG_DEBUG);
技巧二:使用HTTP而非HTTPS快速验证
开发阶段先用HTTP(明文)快速验证API逻辑是否正确,再切换到HTTPS排查TLS问题。这样可以把问题分解为"API逻辑"和"TLS配置"两个独立部分:
c
// 开发环境使用HTTP
#ifdef CONFIG_DEV_MODE
#define API_BASE_URL "http://192.168.1.100:8080/api/v1"
#else
#define API_BASE_URL "https://iot.cztech.cn/api/v1"
#endif
技巧三:服务器端日志比对
当ESP32上报的数据在沧州虎王科技物联网平台上显示异常时,第一步是查看平台后端日志确认数据是否到达、格式是否正确。我们物联网平台提供了请求日志查看功能,可以直接看到ESP32发来的原始HTTP请求体。
九、踩坑经验
坑一:TLS握手内存不足
ESP32默认配置下,TLS握手需要约40KB内存。如果应用本身内存占用高,握手时会因为内存不足而失败,但错误信息可能只是笼统的"connection failed"。解决方案是优化mbedTLS配置,减少fragment size,或者在握手前释放一些不必要的内存。
坑二:HTTP Keep-Alive导致连接复用问题
esp_http_client默认启用Keep-Alive,复用TCP连接。但ESP32的lwIP在连接空闲一段时间后可能被NAT路由器断开,导致下次请求失败。解决方案是设置较短的keep-alive超时,或在请求失败后强制重建连接。
坑三:大JSON响应导致缓冲区溢出
设备配置接口返回的JSON可能很大(包含完整的物模型定义)。我们的做法是在服务器端支持分页和字段过滤,ESP32只请求需要的字段:
c
// 只请求必要字段
api_response_t resp = {0};
api_send_with_retry("GET", "/config?fields=model,properties", NULL, 0, NULL, &resp);
十、总结
ESP32的HTTP+TLS通信开发看似简单,实际涉及的环节很多:WiFi连接管理、NTP时间同步、TLS证书配置、HTTP请求封装、错误重试、内存优化。每一个环节出问题都会导致通信失败。
我们沧州虎王科技技术团队总结的这套方案,核心设计原则是:把复杂性封装在底层,对上层暴露简洁的API 。应用代码只需要调用 api_post_telemetry(json_str),不需要关心WiFi是否连接、TLS是否握手成功、是否需要重试。
配合沧州虎王科技物联网平台的API和ESP32工具箱V2.0的调试能力,我们的ESP32设备已经稳定运行在多个项目中,日均处理数十万次HTTP请求。希望这些经验能帮助到正在做ESP32联网开发的同行们。