测试环境

  • 芯片:ESP32-S3
  • ESP-IDF:v5.5.2

一、HTTP 服务器说明

访问 HTTP 服务器的方法:

  1. 当 ESP32 连上 WIFI 后,路由器会给 ESP32 分配一个 IP 地址,那么只要和这个 IP 地址位于同一个网段内的设备都可以通过 http://: 来访问 ESP32 的 HTTP 服务器。
  2. 当 ESP32 开启 AP 后,当设备连上该 AP 后,ESP32 会给该设备分配一个 IP 地址(当前 ESP-IDF 默认使用的网段是 192.168.4),该设备可以通过 http://192.168.4.1: 来访问 ESP32 的 HTTP 服务器。

当 HTTP 服务器收到请求后,可以:

  1. 返回静态网站的文件内容。

    如果一个静态网站有三个文件:index.html,index.css,index.png,那么浏览器会发送三个请求,HTTP 服务器只需要读取文件内容并返回给浏览器,浏览器会自动渲染该静态网站。

  2. 返回特定的数据。

    如果该 uri 的目的是获取设备当前的温度,那么则直接返回设备当前的温度。

二、示例

HTTPD,即 http daemon。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include "esp_err.h"
#include "esp_log.h"
#include "esp_http_server.h"

static const char *TAG = "httpd";

static esp_err_t priv_uri_index_handle(httpd_req_t *req);

/* http server 服务器句柄 */
static httpd_handle_t s_httpd_handle = NULL;

static const httpd_uri_t s_index_uri = {
    .uri = "/index",                   /* uri 是 "/index" */
    .method = HTTP_GET,                /* uri 仅支持 GET 方法 */
    .handler = priv_uri_index_handle,  /* uri 的回调函数 */
    .user_ctx = NULL,                  /* 用户上下文 */
};

static esp_err_t priv_uri_index_handle(httpd_req_t *req)
{
    esp_err_t err = ESP_OK;

    /* 传入的 uri,这里打印的是 /uri */
    ESP_LOGI(TAG, "uri: %s", req->uri);

    /* 触发该 uri 回调的方法, 这里打印的是 HTTP_GET */
    ESP_LOGI(TAG, "method: %d", req->method);

    /* 此时 HTTP 服务器已经收到了 HTTP Header, 这里尝试接收 HTTP Body */
    int ret = 0;
    char recv_buf[4096] = {0};
    ret = httpd_req_recv(req, recv_buf, 4096);
    if (ret <= 0) {
        return ESP_FAIL;
    }

    /* 处理 recv_buf */
    ...

    /* 配置响应的状态码 */
    httpd_resp_set_status(req, HTTPD_200);

    /* 配置响应的数据类型 */
    httpd_resp_set_type(req, "application/json");

    /* 发送 HTTP 响应 */
    char *send_buf = "{\"code\": 0}"
    err = httpd_resp_send(req, send_buf, strlen(send_buf));
    if (err != ESP_OK) {
        return ESP_FAIL;
    }

    /* 当返回值不是 ESP_OK 时, httpd 会自动断开当前的连接 */
    return ESP_OK;
}

int httpd_init(void)
{
    esp_err_t err = ESP_OK;

    /* http 服务器配置初始化 */
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    /* http 服务器配置 */
    config.lru_purge_enable = true;  /* 开启 LRU 功能 */
    config.max_uri_handlers = 15;    /* 支持 uri 的数量上限是 15 */
    config.server_port = 80          /* 端口号是 80 */
    config.stack_size = 20 * 1024;   /* http 服务区任务的栈空间是 20 KB */

    /* 启动 HTTP 服务器 */
    err = httpd_start(&s_httpd_handle, &config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "start http server failed: %s", esp_err_to_name(err));
        return -1;
    }

    /* 注册 URI: http://<local_host>/index */
    err = httpd_register_uri_handler(s_httpd_handle, &s_index_uri);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "register http uri failed: %s", esp_err_to_name(err));
    }

    return 0;
}

三、其他功能

1. 通配符

在上面的示例可以发现,每创建一个 uri 就要注册一个回调函数。数量少还好,一旦数量多了,就要频繁注册。

1
2
3
4
5
6
/system/info      -> 回调函数 1
/system/version   -> 回调函数 2
/system/login     -> 回调函数 3

/network/wifi     -> 回调函数 4
/network/ethernet -> 回调函数 5

这时候我们可以使用通配符解决该问题:/system/* 可以匹配到 /system/info、/system/version、/system/login 这三个 uri。

1
2
3
/system/*  -> 回调函数 1

/network/* -> 回调函数 2

其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static esp_err_t priv_uri_system_handle(httpd_req_t *req);

static const httpd_uri_t s_system_uri = {
    .uri = "/system/*",                /* uri 是 "/system/*" 系列 */
    .method = HTTP_GET,                /* uri 仅支持 GET 方法 */
    .handler = priv_uri_system_handle, /* uri 的回调函数 */
    .user_ctx = NULL,                  /* 用户上下文 */
};

static esp_err_t priv_uri_system_handle(httpd_req_t *req)
{
    /* 根据具体的 uri 来进一步处理 */
    if (strcmp(req->uri, "/system/info") == 0) {
        return priv_info_handle(req);
    } else if (strcmp(req->uri, "/system/version") == 0) {
        return priv_version_handle(req);
    } else if (strcmp(req->uri, "/system/login") == 0) {
        return priv_login_handle(req);
    }

    httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, NULL);

    return ESP_FAIL;
}

httpd_config_t config = HTTPD_DEFAULT_CONFIG();

/**
 * 默认的匹配函数是 httpd_uri_match_simple
 * 这里将匹配函数改为通配符匹配
 */
config.uri_match_fn = httpd_uri_match_wildcard;

2. 支持多个方法

在上面的示例可以发现,每一个 uri 仅支持一个 method,如果要支持多个 method,需要创建多个相同的 uri,这样麻烦,所以我们可以使用 HTTP_ANY

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static esp_err_t priv_uri_system_handle(httpd_req_t *req);

static const httpd_uri_t s_system_uri = {
    .uri = "/system/*",                /* uri 是 "/system/*" 系列 */
    .method = HTTP_ANY,                /* uri 仅支持任意方法 */
    .handler = priv_uri_system_handle, /* uri 的回调函数 */
    .user_ctx = NULL,                  /* 用户上下文 */
};

static esp_err_t priv_uri_system_handle(httpd_req_t *req)
{
    /* 当前支持的 method 有: GET, POST */
    if ((ret->method != HTTP_GET) && (ret->method != HTTP_POST)) {
        httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED, NULL);
    }

    ...

    httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, NULL);

    return ESP_FAIL;
}

版权声明

本文为「Zeepunt 日常随笔」的原创文章,遵循 CC BY-NC-ND 4.0 许可协议。允许在署名作者、注明原文链接且不作任何更改的前提下非商业性地分享本文。

原文链接:https://zeepunt.github.io/article/esp32/esp32%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-http-%E6%9C%8D%E5%8A%A1%E5%99%A8/