← 返回教程库

HTTP 请求:用 esp_http_client 把数据上传到云

最后更新 2026-06-22
L3 · 联网与 IoT ⏱ 约 24 分钟 🟢 软件/低风险
你将学到
  • 说清 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_msesp_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 取节点,字符串值在 ->valuestringcJSON_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_nameESP_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_LOGIs_resp_buf 看返回到底是什么;返回大就调大 MAX_HTTP_BUF
跑几轮后系统重启 / 内存越用越少 cJSON_DeletecleanupcJSON_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 的天气接口,把响应里嵌套的 tempweather 字段解析出来打印。重点练 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_clientGET 取数据、发 POST 上传数据,全程不依赖任何第三方库。
  • 你理解了和 Arduino 最大的差别:响应 body 是在 _http_event_handlerHTTP_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 那套联网骨架。

想看完整学习路线,回到 学习指南路线图

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明