HTTP 请求:用 esp_http_client 把数据上传到云
- 说清 HTTP 的 GET 与 POST 区别、URL/header/body 的角色,以及 200/404/500 状态码各代表什么
- 用 esp_http_client 写出能跑的 GET(读公开 API)和 POST(发传感器数据),并理解 esp_http_client_perform() 同步发请求的流程
- 看懂事件回调(_http_event_handler 的 HTTP_EVENT_ON_DATA)是怎么把响应 body 一段段攒回来的
- 用 ESP-IDF 内置的 cJSON 组装请求 JSON、解析返回 JSON 取字段,并 cJSON_Delete 防内存泄漏
- 判断什么场景该用 HTTP、什么场景该换 MQTT
板子已经连上 WiFi、拿到 IP 了(如果还没连上,回到 让 ESP32-S3 连上 WiFi 把那套 wifi_init_sta() 骨架跑通——本篇直接复用它,默认你已经连上网、IP_EVENT_STA_GOT_IP 那条日志打出来了)。拿到 IP 看着挺有成就感,可数据还闷在板子里:手机上、电脑上、云后台上什么都看不到。
要让数据「出去」,最朴素、最通用的办法就是 HTTP 请求。你平时刷网页、点开 App,背后跑的全是 HTTP。ESP32-S3 也能当一个迷你浏览器,主动把温湿度 POST 给服务器,或者去某个公开接口 GET 一段数据回来。这一节就把这件事从头到尾跑通——用的是 ESP-IDF 自带的 esp_http_client,不用装任何第三方库。
先把一句话放最前面,这是本篇跟 Arduino 写法最大的不同:ESP-IDF 收响应 body 是「事件回调」式的,不是 Arduino http.getString() 一把梭。 如果你之前用过 Arduino 的 HTTPClient,先把"http.GET() 完直接 getString() 拿整段"的肌肉记忆放一边。在 ESP-IDF 里,body 是分着一段一段从网络上来的,你写一个事件回调函数坐等系统把每一段塞给你——这个范式,和上一篇 WiFi 的「事件驱动」是一脉相承的。
读这篇前,除了已联网,你还得熟悉 idf.py build flash monitor 看日志、看得懂 ESP_LOGI 打出来的串口输出。本篇不接任何线,纯软件。
HTTP 到底在干什么
把 HTTP 想成「点菜」:你(ESP32-S3)是客人,服务器是后厨。你递一张点菜单(请求),后厨做好端出来(响应)。一来一回,一次请求结束。
点菜单上有几样东西:
- 方法(method):最常用两个。
GET是「我要拿点东西」,比如取当前时间、取天气;POST是「我要交点东西上去」,比如把温湿度提交给服务器。 - URL:地址,告诉请求发去哪。比如
http://httpbin.org/post。在esp_http_client里它就是config.url那一项。 - header(请求头):附加说明,像备注栏。最常见的是
Content-Type: application/json,意思是「我交上去的内容是 JSON 格式」,对应esp_http_client_set_header()。 - body(请求体):真正要交上去的内容。
GET一般没有 body,POST的 body 里放你要发的数据,对应esp_http_client_set_post_field()。
后厨端出来的响应里,最该看的是状态码,用 esp_http_client_get_status_code() 取:
200—— 成功,一切正常。404—— 找不到,URL 写错了或接口不存在。500—— 服务器自己出错了,跟你的板子没关系,等会儿再试或换接口。
还有一层是 esp_http_client_perform() 的返回值 esp_err_t:它是 ESP_OK 只代表「这次请求-响应的流程没崩」(连上了、收完了),状态码是不是 200 得另外问 get_status_code。这俩别混——流程 OK 不等于业务成功(比如 perform 成功但状态码是 404)。
记住:perform() 返回 ESP_OK 且状态码 200,才算真成功,其他都要排查。
第一步:GET 去取一段数据
先做简单的——去一个公开接口取数据。下面用 worldtimeapi.org 取当前时间,它返回 JSON。
关键点先说:esp_http_client 收响应 body 的标准姿势是注册一个事件回调 _http_event_handler,在 HTTP_EVENT_ON_DATA 事件里把一段段数据攒进自己的缓冲区。下面这段是完整可跑的(前提是 wifi_init_sta() 已经把网连上):
#include "esp_http_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
static const char *TAG = "http";
#define MAX_HTTP_BUF 2048
static char s_resp_buf[MAX_HTTP_BUF + 1]; // 攒响应 body 的缓冲区
static int s_resp_len = 0;
// 事件回调:系统把请求生命周期里的每个事件交给你,body 在 ON_DATA 里一段段来
static esp_err_t _http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_DATA:
// body 不是一次到齐的,可能分多次回调;这里累加到缓冲区
if (s_resp_len + evt->data_len <= MAX_HTTP_BUF) {
memcpy(s_resp_buf + s_resp_len, evt->data, evt->data_len);
s_resp_len += evt->data_len;
s_resp_buf[s_resp_len] = '\0'; // 收尾留个 '\0' 好当字符串用
} else {
ESP_LOGW(TAG, "响应超过缓冲区 %d 字节,被截断", MAX_HTTP_BUF);
}
break;
default:
break;
}
return ESP_OK;
}
void http_get_time(void) {
s_resp_len = 0; // 每次请求前清空缓冲
s_resp_buf[0] = '\0';
esp_http_client_config_t config = {
.url = "http://worldtimeapi.org/api/timezone/Asia/Shanghai",
.event_handler = _http_event_handler,
.timeout_ms = 8000, // 超时,别让它无限期卡着
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_err_t err = esp_http_client_perform(client); // 同步发出请求并收完响应
if (err == ESP_OK) {
int code = esp_http_client_get_status_code(client);
int len = esp_http_client_get_content_length(client);
ESP_LOGI(TAG, "状态码=%d 内容长度=%d", code, len);
if (code == 200) {
ESP_LOGI(TAG, "原始返回: %s", s_resp_buf);
// 用 cJSON 解析,只取 datetime 字段
cJSON *root = cJSON_Parse(s_resp_buf);
if (root != NULL) {
cJSON *dt = cJSON_GetObjectItem(root, "datetime");
if (cJSON_IsString(dt)) {
ESP_LOGI(TAG, "当前时间: %s", dt->valuestring);
}
cJSON_Delete(root); // 解析完一定要 Delete,否则内存泄漏
} else {
ESP_LOGE(TAG, "JSON 解析失败,返回的可能不是合法 JSON");
}
}
} else {
ESP_LOGE(TAG, "请求失败: %s", esp_err_to_name(err)); // 连不上/超时走这
}
esp_http_client_cleanup(client); // 用完一定要 cleanup 释放 client
}
在 app_main 里,先把 WiFi 连上、再调它:
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
wifi_init_sta(); // 复用上一篇的骨架,阻塞到连上 WiFi
http_get_time(); // 连上之后再发 HTTP,顺序别反
}
几个关键点,一个个讲透:
esp_http_client_config_t是一张配置卡:填url、挂上event_handler、设timeout_ms。esp_http_client_init(&config)拿到一个client句柄,后面所有操作都围着它转。esp_http_client_perform(client)才是真正「发出」:它是同步的——这一句会一直阻塞到整个请求-响应跑完(包括所有HTTP_EVENT_ON_DATA回调都回调完)才返回。它返回的是esp_err_t(流程成没成),不是状态码。- body 在回调里攒,不是
getString()一把拿:这是和 Arduino 最大的差别。HTTP_EVENT_ON_DATA可能被调用多次(响应大的时候分块来),所以代码里用s_resp_len累加、memcpy拼接。别假设一次回调就拿到全部 body。 esp_http_client_get_status_code()单独问状态码,get_content_length()拿声明的内容长度。- 解析用 cJSON(ESP-IDF 内置,不用装库):
cJSON_Parse()把字符串变成树,cJSON_GetObjectItem(root,"datetime")按 key 取节点,字符串值在->valuestring。cJSON_Delete(root)必须调——cJSON 在堆上分配,不 Delete 就泄漏,循环里发请求几轮就把内存吃光。 esp_http_client_cleanup(client)别忘:和 cJSON_Delete 一样是「成对释放」的纪律,client 不释放,多发几次连接句柄越积越多最后崩。
你应该看到什么
idf.py monitor 的日志里依次出现(中间夹着系统自己打的底层日志,正常):
I (3210) http: 状态码=200 内容长度=312
I (3215) http: 原始返回: {"abbreviation":"CST","datetime":"2026-06-20T15:42:08.123456+08:00", ...}
I (3220) http: 当前时间: 2026-06-20T15:42:08.123456+08:00
看到 状态码=200 和一段带 datetime 的 JSON,GET 就通了。
第二步:POST 把传感器数据交上去
取数据会了,反过来把数据发出去。这里用 httpbin.org/post——一个专门给人测试的「回声」服务器,你发什么它原样返回什么,不用自己搭后端就能验证。
POST 和 GET 的代码骨架一样(同一个 client 句柄、同一个事件回调收返回),多三步:设方法为 POST、设 body、设 Content-Type 头。body 用 cJSON 组装:
void http_post_sensor(float temp, float humi) {
s_resp_len = 0;
s_resp_buf[0] = '\0';
// 1) 用 cJSON 组装要发的 JSON:{"device":"esp32s3-01","temp":26.5,"humi":58.0}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device", "esp32s3-01");
cJSON_AddNumberToObject(root, "temp", temp);
cJSON_AddNumberToObject(root, "humi", humi);
char *post_data = cJSON_PrintUnformatted(root); // 转成紧凑 JSON 字符串(malloc出来的)
ESP_LOGI(TAG, "即将发送: %s", post_data);
esp_http_client_config_t config = {
.url = "http://httpbin.org/post",
.event_handler = _http_event_handler,
.timeout_ms = 8000,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// 2) POST 三件套:设方法、设 body、声明 Content-Type
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_post_field(client, post_data, strlen(post_data));
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
int code = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "状态码=%d", code);
if (code == 200) {
ESP_LOGI(TAG, "服务器返回: %s", s_resp_buf);
}
} else {
ESP_LOGE(TAG, "POST 失败: %s", esp_err_to_name(err));
}
// 3) 三处释放,一个都不能漏
esp_http_client_cleanup(client);
cJSON_free(post_data); // cJSON_PrintUnformatted 返回的是 malloc 的,要 free
cJSON_Delete(root); // 对象树本身也要 Delete
}
和 GET 比,POST 多的就是中间「2)」那三行:
esp_http_client_set_method(client, HTTP_METHOD_POST)—— 把方法从默认的 GET 改成 POST。esp_http_client_set_post_field(client, post_data, strlen(post_data))—— 把要发的 body 和它的长度交给 client。esp_http_client_set_header(client, "Content-Type", "application/json")—— 告诉服务器「我发的是 JSON」,不声明的话很多后端会按表单解析、拿不到你的数据。
还有一处容易漏的内存纪律:cJSON_PrintUnformatted() 返回的字符串是 cJSON 用 malloc 分配的,用完要 cJSON_free(post_data);对象树 root 另外用 cJSON_Delete(root) 释放。两个是两块内存,别只释放一个。
你应该看到什么
I (5120) http: 即将发送: {"device":"esp32s3-01","temp":26.5,"humi":58}
I (5680) http: 状态码=200
I (5685) http: 服务器返回: { "json": { "device": "esp32s3-01", "humi": 58, "temp": 26.5 }, ... }
httpbin 把你发的内容塞在返回的 json 字段里原样吐回来——这说明服务器完整收到了你发的数据。换成你自己的服务器时,config.url 改成你的接口地址即可,body 的字段跟后端商量好。
关于 HTTPS 一句话
上面用的都是 http://(明文)。真正上生产、发到公网服务器,几乎都要 https://(加密)。esp_http_client 支持 HTTPS,但要在 config 里带上服务器证书:把目标站点的 CA 根证书编进固件,配到 config.cert_pem(或用证书包 esp_crt_bundle_attach)。证书这块有点啰嗦——怎么拿证书、怎么嵌进工程、怎么处理证书过期,单独说更清楚,细节留给 HTTPS 与设备安全。本篇先记住「自测用 http://、上线换 https:// 并带证书」这个原则就够了。
HTTP 还是 MQTT,怎么选
HTTP 不是唯一的上云方式,很多教程会让你直接上 MQTT。两者的分工很清楚:
| 场景 | 选 HTTP | 选 MQTT |
|---|---|---|
| 每隔几分钟传一次温湿度 | ✅ 合适 | 也行但偏重 |
| 取一次天气/时间这种「请求-响应」 | ✅ 天生适合 | 不擅长 |
| 每秒上传、要求实时 | ❌ 每次都建连,太费 | ✅ 长连接,高频低延迟 |
| 服务器要主动「推」消息给板子 | ❌ HTTP 是板子主动问 | ✅ 订阅即收,双向 |
| 一个传感器对多个接收方 | ❌ 得自己群发 | ✅ 发布/订阅天生支持 |
一句话:偶尔上传、一问一答用 HTTP;高频、实时、双向、要被服务器推送,用 MQTT。这一节先把 HTTP 跑顺,下一节专门讲 用 MQTT 上云。
故障排查表
| 现象 / 日志 | 多半原因 | 怎么办 |
|---|---|---|
perform 返回非 OK,esp_err_to_name 是 ESP_ERR_HTTP_CONNECT |
网络没就绪 / DNS 没解析出来 | 确认 WiFi 已连(先确认 IP_EVENT_STA_GOT_IP 那条日志出来了),HTTP 一定在拿到 IP 之后再发 |
| 请求一直卡住、超时 | 服务器响应慢 / 端口被墙 | config.timeout_ms 设小点(如 5000)让它早点报错;换一个国内能访问的接口测试 |
状态码 404 |
URL 写错了 | 复制完整 URL 到浏览器里能不能打开;检查路径拼写 |
状态码 400 |
服务器嫌你发的格式不对 | 确认 POST 加了 set_header("Content-Type","application/json");确认 body 是合法 JSON |
状态码 500 |
服务器端出错 | 不是你的问题,换接口或稍后重试 |
cJSON_Parse 返回 NULL(解析失败) |
返回不是 JSON / body 被截断 | 先 ESP_LOGI 打 s_resp_buf 看返回到底是什么;返回大就调大 MAX_HTTP_BUF |
| 跑几轮后系统重启 / 内存越用越少 | cJSON_Delete、cleanup、cJSON_free 漏了 |
逐个核对:每个 cJSON_Parse/CreateObject 对应一个 Delete,每个 PrintUnformatted 对应一个 free,每个 init 对应一个 cleanup |
用 https:// 直接连接失败 |
没配证书 | HTTPS 必须带 CA 证书,先用 http:// 自测,证书细节见 l3-security |
两个变体
变体一:POST 真实的 DHT11 读数。 把上面的假数据换成传感器实测值。先按 DHT11 那节接好线、读到温湿度,再调 http_post_sensor(t, h)。注意 DHT11 读数偶尔会是 nan(读取失败),发之前判断一下 if (!isnan(t) && !isnan(h)) 再发,免得发一堆无效数据上去。
变体二:GET 取天气并解析多层 JSON。 找一个返回 JSON 的天气接口,把响应里嵌套的 temp、weather 字段解析出来打印。重点练 cJSON 的多层取值——JSON 嵌套时一层层往里取:
cJSON *root = cJSON_Parse(s_resp_buf);
cJSON *data = cJSON_GetObjectItem(root, "data"); // 先取外层 data 对象
cJSON *temp = cJSON_GetObjectItem(data, "temp"); // 再从 data 里取 temp
if (cJSON_IsNumber(temp)) ESP_LOGI(TAG, "温度=%.1f", temp->valuedouble);
cJSON_Delete(root); // 注意:只 Delete 根节点,子节点是它的一部分,别重复 Delete
记住一个 cJSON 的内存规则:cJSON_GetObjectItem 拿到的子节点是 root 这棵树的一部分,只 cJSON_Delete(root) 一次就够,别去 Delete 子节点(那会重复释放、崩溃)。
动手挑战
把前面零碎的代码拼成一个完整程序:连上 WiFi 后,每 30 秒读一次 DHT11,把温湿度 POST 到 httpbin.org/post,日志里打印状态码和返回的 temp 字段。
实现要点:
- 别在
app_main里用vTaskDelay(pdMS_TO_TICKS(30000))死等整个任务——更产品的做法是建一个 FreeRTOS 任务(xTaskCreate)专门跑这个上报循环,任务里while(1)+vTaskDelay(pdMS_TO_TICKS(30000)),这样不卡住其他逻辑。 - 发之前判
isnan,无效读数跳过本轮。 - 每轮请求里
cJSON_Delete/cJSON_free/esp_http_client_cleanup一个都不能漏——这是最容易在循环里栽的坑,漏一个,跑几个小时内存就耗光重启。可以用esp_get_free_heap_size()在每轮打一下剩余堆,盯着它是不是稳定、有没有持续往下掉。 - 发完打印状态码和服务器返回里的
temp字段,自己核对发出去的和收回来的对不对得上。
跑通后你就有了一条最朴素的「设备→云」数据链路。把 httpbin 换成你自己的后端 URL,这套代码就能直接用在真项目里。
本篇代码为参考实现,需结合你所用的最新 ESP-IDF 文档自校,尤其是
esp_http_client的事件回调字段、cJSON 的 API 细节随版本可能微调,以官方文档为准。
小结 · 你现在掌握了什么
- 你能用 ESP-IDF 自带的
esp_http_client发 GET 取数据、发 POST 上传数据,全程不依赖任何第三方库。 - 你理解了和 Arduino 最大的差别:响应 body 是在
_http_event_handler的HTTP_EVENT_ON_DATA事件里一段段攒回来的,而esp_http_client_perform()是同步阻塞到收完才返回。 - 你会用 cJSON(ESP-IDF 内置)组装请求 JSON(
cJSON_CreateObject+AddStringToObject/AddNumberToObject+PrintUnformatted)和解析返回 JSON(cJSON_Parse+GetObjectItem+valuestring/valuedouble),并牢记cJSON_Delete/cJSON_free/esp_http_client_cleanup三处释放防泄漏。 - 你会区分
perform()的esp_err_t(流程成没成)和get_status_code()(业务 200 没),会照故障排查表定位问题,知道 HTTPS 要带证书。
HTTP 适合「偶尔上传、一问一答」。但如果你要做实时控制——比如手机上点一下,板子上的灯立刻亮——HTTP 这种「板子主动问」的模式就不够用了,该换 MQTT。下一节:用 MQTT 上云:实时双向通信,它和本篇一样复用 l3-wifi 那套联网骨架。