3、HTTP客户端请求:从协议到实战

HTTP协议,说白了就是设备之间「对话」的规矩。你想想看,浏览器怎么从服务器拿到天气数据?ESP32怎么把传感器值发到云端?背后都是HTTP在干活。这一章,我们就把它彻底搞明白。

3.1 HTTP协议到底是个啥

HTTP全称是超文本传输协议。名字听着唬人,本质就两件事:请求和响应。客户端发个请求,服务器回个响应,完事。

我刚开始做嵌入式联网时,总觉得HTTP是浏览器才用的东西。后来发现,ESP32这种小芯片,照样能当HTTP客户端用。而且用得还挺溜。

3.1.1 请求报文长啥样

一个典型的GET请求,拆开看就这几部分:

GET /api/weather?city=beijing HTTP/1.1
Host: api.weather.com
User-Agent: ESP32-Client/1.0
Accept: application/json

第一行是请求行,包含方法(GET)、路径(/api/weather...)、协议版本(HTTP/1.1)。后面是请求头,每行一个键值对。最后还有个空行,表示头部结束。

核心要点:HTTP请求就是纯文本。你用串口打印出来,人眼直接能看懂。这也是它调试起来特别方便的原因。

3.1.2 响应报文的结构

服务器回给你的,也是纯文本:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 128

{"temp":25,"humidity":60,"city":"beijing"}

第一行是状态行,包含版本、状态码(200)、原因短语(OK)。后面是响应头,空行之后是响应体——也就是我们真正想要的数据。

状态码 含义 常见场景
200 OK 请求成功,数据在响应体里
301/302 重定向 资源地址变了,需要重新请求
400 Bad Request 你发的请求格式不对
401 Unauthorized 没带认证信息,或者认证失败
404 Not Found 路径写错了,资源不存在
500 Internal Server Error 服务器自己炸了,跟你没关系

我曾经踩过的坑:有一次ESP32请求一直返回400,我查了半天代码,最后发现是URL里多了个空格。HTTP协议对格式极其敏感,多一个空格、少一个换行,都可能出问题。

3.2 ESP32发送GET请求

好,理论说完了,咱们直接上手。ESP32用ESP-IDF框架发HTTP请求,主要靠esp_http_client这个组件。

3.2.1 环境准备

首先,确保你的ESP-IDF版本在4.0以上。我建议用4.4 LTS版本,稳定,坑少。

在menuconfig里,需要开启:

  • Component config → HTTP Client → Enable HTTP Client
  • Component config → LWIP → Enable HTTP Client(如果用的是LWIP)

小提示:如果你用的是ESP-IDF 5.0以上版本,HTTP客户端默认就是开启的。但建议还是检查一下,省得到时候编译报错一脸懵。

3.2.2 写一个最简单的GET请求

直接上代码。这个例子请求一个公开的天气API,拿到JSON格式的数据:

#include "esp_http_client.h"
#include "esp_log.h"

static const char *TAG = "HTTP_CLIENT";

void http_get_example(void)
{
    esp_http_client_config_t config = {
        .url = "http://api.weather.com/v1/current?city=beijing",
        .method = HTTP_METHOD_GET,
        .timeout_ms = 5000,
    };

    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 status_code = esp_http_client_get_status_code(client);
        int content_length = esp_http_client_get_content_length(client);
        ESP_LOGI(TAG, "HTTP Status = %d, content_length = %d",
                 status_code, content_length);
    } else {
        ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
    }

    esp_http_client_cleanup(client);
}

这段代码干了三件事:

  1. 配置:设置URL、方法、超时时间
  2. 执行:调用esp_http_client_perform发送请求并等待响应
  3. 清理:释放资源

我个人习惯把超时时间设成5秒。太短了网络波动容易失败,太长了卡在那里影响用户体验。

3.2.3 读取响应体

上面的代码只拿到了状态码和长度,没拿到真正的数据。要读响应体,需要注册一个回调函数:

esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    switch (evt->event_id) {
        case HTTP_EVENT_ON_DATA:
            ESP_LOGI(TAG, "Received %d bytes", evt->data_len);
            // evt->data 指向接收到的数据
            // evt->data_len 是数据长度
            // 你可以在这里把数据存到缓冲区
            break;
        default:
            break;
    }
    return ESP_OK;
}

void http_get_with_response(void)
{
    esp_http_client_config_t config = {
        .url = "http://api.weather.com/v1/current?city=beijing",
        .method = HTTP_METHOD_GET,
        .event_handler = _http_event_handler,
        .buffer_size = 1024,
    };

    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_http_client_perform(client);
    esp_http_client_cleanup(client);
}

注意buffer_size这个参数。它决定了ESP32一次能接收多少数据。如果响应体超过这个值,事件处理器会被多次调用,每次拿到一部分数据。你需要自己拼接。

注意:HTTP_EVENT_ON_DATA事件可能被触发多次。千万别假设一次就能收完所有数据。我见过有人在这个坑里栽了跟头——只处理了第一次回调,结果数据不全。

3.3 解析JSON响应

拿到响应体之后,下一步就是解析JSON。ESP-IDF自带cJSON库,轻量又好用。

3.3.1 cJSON基础用法

假设我们收到了这样的JSON:

{
    "temp": 25,
    "humidity": 60,
    "city": "beijing",
    "wind": {
        "speed": 3.5,
        "direction": "north"
    }
}

解析代码:

#include "cJSON.h"

void parse_weather_data(const char *json_string)
{
    cJSON *root = cJSON_Parse(json_string);
    if (root == NULL) {
        ESP_LOGE(TAG, "JSON parse failed");
        return;
    }

    cJSON *temp = cJSON_GetObjectItem(root, "temp");
    cJSON *humidity = cJSON_GetObjectItem(root, "humidity");
    cJSON *city = cJSON_GetObjectItem(root, "city");

    if (cJSON_IsNumber(temp)) {
        ESP_LOGI(TAG, "Temperature: %.1f", temp->valuedouble);
    }
    if (cJSON_IsNumber(humidity)) {
        ESP_LOGI(TAG, "Humidity: %d", humidity->valueint);
    }
    if (cJSON_IsString(city) && city->valuestring != NULL) {
        ESP_LOGI(TAG, "City: %s", city->valuestring);
    }

    // 解析嵌套对象
    cJSON *wind = cJSON_GetObjectItem(root, "wind");
    if (wind != NULL) {
        cJSON *speed = cJSON_GetObjectItem(wind, "speed");
        if (cJSON_IsNumber(speed)) {
            ESP_LOGI(TAG, "Wind speed: %.1f", speed->valuedouble);
        }
    }

    cJSON_Delete(root);
}

几个关键点:

  • cJSON_Parse:把字符串解析成cJSON对象树
  • cJSON_GetObjectItem:按key取值
  • cJSON_IsNumber/cJSON_IsString:类型检查,防止崩溃
  • cJSON_Delete:释放内存,别忘了

我的习惯:每次调用cJSON_GetObjectItem之后,都检查返回值是不是NULL。API返回的数据结构可能变,你永远不知道服务器哪天就改了字段名。加个检查,省得半夜被叫起来修bug。

3.3.2 处理数组

有时候JSON里会有数组,比如:

{
    "forecast": [
        {"day": "周一", "temp_high": 30, "temp_low": 20},
        {"day": "周二", "temp_high": 28, "temp_low": 18},
        {"day": "周三", "temp_high": 32, "temp_low": 22}
    ]
}

解析数组用cJSON_GetArrayItem和cJSON_GetArraySize:

void parse_forecast(const char *json_string)
{
    cJSON *root = cJSON_Parse(json_string);
    cJSON *forecast = cJSON_GetObjectItem(root, "forecast");

    if (cJSON_IsArray(forecast)) {
        int size = cJSON_GetArraySize(forecast);
        for (int i = 0; i < size; i++) {
            cJSON *item = cJSON_GetArrayItem(forecast, i);
            cJSON *day = cJSON_GetObjectItem(item, "day");
            cJSON *high = cJSON_GetObjectItem(item, "temp_high");

            if (cJSON_IsString(day)) {
                ESP_LOGI(TAG, "Day %d: %s, High: %.0f",
                         i, day->valuestring, high->valuedouble);
            }
        }
    }

    cJSON_Delete(root);
}

3.4 完整流程:一张图看懂

说了这么多,咱们用一张SVG图把整个流程串起来:

ESP32 HTTP客户端请求完整流程 1. 配置HTTP客户端 2. 建立TCP连接 3. 发送GET请求 4. 接收响应头 5. 读取响应体 6. 解析JSON数据 关键说明: • 步骤1-3是请求阶段,ESP32主动发起 • 步骤4-6是响应阶段,ESP32被动接收并处理 • 整个流程是同步的:发完请求,等响应,再解析 ⚠️ 常见问题: • 响应体可能分多次到达,别假设一次收完 • JSON解析失败时,先打印原始字符串看看

3.5 实战中的坑与技巧

代码写完了,流程也清楚了。但实际项目中,总会遇到一些意想不到的问题。我分享几个亲身经历:

3.5.1 内存管理

ESP32的RAM有限,尤其是PSRAM没开启的时候。cJSON解析大JSON时,内存消耗可能超出预期。

我的建议:

  • 解析完数据后,立即调用cJSON_Delete释放内存
  • 如果JSON很大,考虑流式解析,不要一次性加载到内存
  • 用heap_caps_get_free_size监控内存使用情况

3.5.2 网络超时处理

ESP32连WiFi,网络环境千差万别。有时候服务器响应慢,有时候WiFi信号差。

我一般这样处理:

esp_http_client_config_t config = {
    .url = "http://api.weather.com/v1/current",
    .timeout_ms = 10000,  // 10秒超时
    .keep_alive_enable = true,  // 启用keep-alive
};

超时时间设10秒,够用。keep-alive开启后,多次请求复用同一个TCP连接,速度快不少。

3.5.3 HTTPS怎么办

现在很多API都强制HTTPS。ESP32支持HTTPS,但需要配置证书。

最简单的做法:

esp_http_client_config_t config = {
    .url = "https://api.weather.com/v1/current",
    .cert_pem = server_cert_pem_start,  // 证书内容
};

证书可以从服务器获取,或者用esp_crt_bundle自动加载。我个人建议用证书捆绑包,省事,而且支持大多数公共API。

总结一下:HTTP客户端请求,说白了就是「配置→连接→发送→接收→解析」这五步。每一步都有坑,但踩过一次就记住了。ESP32的HTTP客户端组件封装得不错,用起来比想象中简单。关键是理解协议本身,代码反而是次要的。