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);
}
这段代码干了三件事:
- 配置:设置URL、方法、超时时间
- 执行:调用
esp_http_client_perform发送请求并等待响应 - 清理:释放资源
我个人习惯把超时时间设成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图把整个流程串起来:
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客户端组件封装得不错,用起来比想象中简单。关键是理解协议本身,代码反而是次要的。