* @file http_request.c
* @brief 基于 FreeRTOS 队列的 HTTP/HTTPS 请求组件
* @version 0.2
* @date 2026-09-22
*/
#include "http_request.h"
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_crt_bundle.h"
#define TAG "http_request"
#define HTTP_REQUEST_TASK_NAME "http_request_task"
#define HTTP_REQUEST_TASK_STACK 6144
#define HTTP_REQUEST_TASK_PRIORITY 5
static QueueHandle_t s_queue = NULL;
static TaskHandle_t s_task = NULL;
static esp_http_client_handle_t s_client = NULL;
* @brief 字符串拷贝到堆,入参为 NULL 时返回 NULL
*/
static char *http_request_strdup(const char *src)
{
if (src == NULL) {
return NULL;
}
size_t len = strlen(src) + 1;
char *dst = malloc(len);
if (dst != NULL) {
memcpy(dst, src, len);
}
return dst;
}
* @brief 释放队列元素内申请的内存
*/
static void http_request_item_clear(http_request_item_t *item)
{
free(item->url);
free(item->post_data);
free(item->content_type);
item->url = NULL;
item->post_data = NULL;
item->content_type = NULL;
}
* @brief 循环接收响应体并分片回调
*
* @note 不依赖 Content-Length,服务器返回 chunked 编码(此时长度为 -1)也能正常接收
*/
static http_request_result_t http_request_read_body(esp_http_client_handle_t client,
const http_request_item_t *item)
{
char *buffer = (char *)malloc(item->chunk_size + 1);
if (buffer == NULL) {
ESP_LOGW(TAG, "申请接收内存失败, 长度=%d", item->chunk_size);
return HTTP_REQUEST_FAIL_NO_MEM;
}
int64_t total_len = esp_http_client_get_content_length(client);
int64_t read_total = 0;
http_request_result_t result = HTTP_REQUEST_OK;
while (1) {
int read_len = esp_http_client_read(client, buffer, item->chunk_size);
if (read_len < 0) {
ESP_LOGW(TAG, "接收响应数据出错, ret=%d", read_len);
result = HTTP_REQUEST_FAIL_RECV;
break;
}
if (read_len == 0) {
if (!esp_http_client_is_complete_data_received(client)) {
ESP_LOGW(TAG, "响应数据不完整, 已接收 %lld 字节", (long long)read_total);
result = HTTP_REQUEST_FAIL_RECV;
}
break;
}
buffer[read_len] = '\0';
read_total += read_len;
if (item->on_data != NULL) {
item->on_data(buffer, read_len, total_len);
}
}
free(buffer);
ESP_LOGI(TAG, "响应接收完成, 共 %lld 字节", (long long)read_total);
return result;
}
* @brief 处理一条队列消息,完成一次完整的 HTTP 请求
*/
static void http_request_handle(esp_http_client_handle_t client, http_request_item_t *item)
{
http_request_result_t result = HTTP_REQUEST_OK;
int status_code = 0;
const char *content_type = item->content_type;
int body_len = (item->post_data != NULL) ? (int)strlen(item->post_data) : 0;
esp_http_client_set_method(client, item->method);
esp_http_client_set_timeout_ms(client, item->timeout_ms);
if (esp_http_client_set_url(client, item->url) != ESP_OK) {
ESP_LOGW(TAG, "请求链接非法: %s", item->url);
result = HTTP_REQUEST_FAIL_PARAM;
goto done;
}
if (content_type == NULL && body_len > 0) {
content_type = HTTP_REQUEST_CONTENT_TYPE_DEFAULT;
}
if (content_type != NULL) {
esp_http_client_set_header(client, "Content-Type", content_type);
}
if (esp_http_client_open(client, body_len) != ESP_OK) {
ESP_LOGW(TAG, "建立 http 连接失败: %s", item->url);
result = HTTP_REQUEST_FAIL_OPEN;
goto done;
}
if (body_len > 0) {
int written = esp_http_client_write(client, item->post_data, body_len);
if (written != body_len) {
ESP_LOGW(TAG, "发送请求数据失败, 已发送 %d/%d 字节", written, body_len);
result = HTTP_REQUEST_FAIL_WRITE;
goto close;
}
}
if (esp_http_client_fetch_headers(client) < 0) {
ESP_LOGW(TAG, "获取响应头失败: %s", item->url);
result = HTTP_REQUEST_FAIL_HEADER;
goto close;
}
status_code = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "响应码 ResponseCode= %d", status_code);
result = http_request_read_body(client, item);
close:
esp_http_client_close(client);
done:
if (item->on_done != NULL) {
item->on_done(result, status_code);
}
http_request_item_clear(item);
}
* @brief HTTP 请求任务,串行处理队列中的请求
*/
static void http_request_task(void *arg)
{
QueueHandle_t queue = (QueueHandle_t)arg;
const esp_http_client_config_t config = {
.timeout_ms = HTTP_REQUEST_TIMEOUT_DEFAULT,
.buffer_size = HTTP_REQUEST_CHUNK_DEFAULT,
.crt_bundle_attach = esp_crt_bundle_attach,
};
s_client = esp_http_client_init(&config);
if (s_client == NULL) {
ESP_LOGE(TAG, "http 客户端初始化失败, 任务退出");
s_task = NULL;
vTaskDelete(NULL);
return;
}
s_queue = queue;
ESP_LOGI(TAG, "http 客户端初始化成功, 等待请求");
http_request_item_t item;
while (1) {
if (xQueueReceive(queue, &item, portMAX_DELAY) == pdTRUE) {
http_request_handle(s_client, &item);
}
}
}
* @brief 初始化 HTTP 客户端
*
* @param QueueHandle 传入 http 请求结构 队列
*/
void http_request_init(QueueHandle_t QueueHandle)
{
if (QueueHandle == NULL) {
ESP_LOGE(TAG, "请求队列为空");
return;
}
if (s_task != NULL) {
ESP_LOGW(TAG, "组件已初始化, 忽略本次调用");
return;
}
if (xTaskCreate(http_request_task, HTTP_REQUEST_TASK_NAME, HTTP_REQUEST_TASK_STACK,
(void *)QueueHandle, HTTP_REQUEST_TASK_PRIORITY, &s_task) != pdPASS) {
s_task = NULL;
ESP_LOGE(TAG, "创建 http 任务失败");
}
}
esp_err_t http_request_submit(const http_request_config_t *config)
{
if (config == NULL || config->url == NULL || config->url[0] == '\0') {
ESP_LOGW(TAG, "请求参数非法");
return ESP_ERR_INVALID_ARG;
}
if (s_queue == NULL) {
ESP_LOGW(TAG, "组件未初始化");
return ESP_ERR_INVALID_STATE;
}
http_request_item_t item = {
.method = config->method,
.chunk_size = (config->chunk_size > 0) ? config->chunk_size : HTTP_REQUEST_CHUNK_DEFAULT,
.timeout_ms = (config->timeout_ms > 0) ? config->timeout_ms : HTTP_REQUEST_TIMEOUT_DEFAULT,
.on_data = config->on_data,
.on_done = config->on_done,
};
item.url = http_request_strdup(config->url);
item.post_data = http_request_strdup(config->post_data);
item.content_type = http_request_strdup(config->content_type);
if (item.url == NULL) {
http_request_item_clear(&item);
return ESP_ERR_NO_MEM;
}
if (xQueueSend(s_queue, &item, 0) != pdTRUE) {
ESP_LOGW(TAG, "请求队列已满, 丢弃请求: %s", config->url);
http_request_item_clear(&item);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
esp_err_t http_request_get(const char *url, http_request_callback_t on_data,
http_request_done_callback_t on_done)
{
const http_request_config_t config = {
.url = url,
.method = HTTP_METHOD_GET,
.on_data = on_data,
.on_done = on_done,
};
return http_request_submit(&config);
}
esp_err_t http_request_post(const char *url, const char *post_data, http_request_callback_t on_data,
http_request_done_callback_t on_done)
{
const http_request_config_t config = {
.url = url,
.method = HTTP_METHOD_POST,
.post_data = post_data,
.on_data = on_data,
.on_done = on_done,
};
return http_request_submit(&config);
}
void http_request_deinit(void)
{
if (s_task != NULL) {
vTaskDelete(s_task);
s_task = NULL;
}
if (s_queue != NULL) {
http_request_item_t item;
while (xQueueReceive(s_queue, &item, 0) == pdTRUE) {
http_request_item_clear(&item);
}
s_queue = NULL;
}
if (s_client != NULL) {
esp_http_client_cleanup(s_client);
s_client = NULL;
}
}