ESP-idf注册双服务器配置
在开发ESP服务器时遇到一个问题,如果开启了mjpeg视频流,再开发其它功能会被阻塞,例如想在网页中新增一个按钮,点击一下可以在串口打印文字,这个会被阻塞住。
(索引):21 POST https://192.168.5.100/test-button net::ERR_TIMED_OUT
(索引):30 网络错误: TypeError: Failed to fetchat testButton ((索引):21:9)at HTMLButtonElement.onclick ((索引):17:36)
MJPEG 流(multipart/x-mixed-replace)不是一次性响应,而是 HTTP 长连接。浏览器发 GET /stream,ESP32 一直不关闭连接,持续推送 JPEG 帧。在 ESP-IDF 的 esp_http_server 里,每一个客户端请求对应一条 TCP socket。这个 socket 不释放,直到浏览器主动断开或者 ESP 出错才会释放。结果就会造成只要页面开着,/stream 连接就是“永久占用”,导致页面中的其它服务始终排不上队,连接超时。
一个比较好的方法是创建两个服务器。
注意,在ESP-idf使用中,esp_http_server
和esp_https_server
是两个不一样的服务器配置包,当前服务器使用https服务器,因此应当主要使用esp_https_server。但注意,这两个包并不是完全并列的,esp_https_server 基于 esp_http_server 实现,在 HTTP 服务器之上增加 TLS/SSL 能力,HTTP 服务器的大部分文档与 API 也适用于 HTTPS 服务器。
像 conf.port_secure/port_insecure
、transport_mode
、证书与密钥字段、会话票据、ALPN、证书选择钩子、TLS 握手超时等,都是 esp_https_server 特有或扩展的配置项;而 httpd_config_t.httpd
则承载通用的 HTTP 层配置(任务与资源等)
对于我们尤其要注意的,两个服务器的监听端口也必须不同(HTTP 用 httpd_config_t.server_port
;而我们的HTTPS 用 httpd_ssl_config_t.port_secure
或 port_insecure
)
如何在一个程序里启动两个https服务器
可以肯定的是,一个程序里可以启动两个https服务器,但必须配置好conf.port_secure
和conf.httpd.ctrl_port
,前者用于配置监听端口,后者是服务器内部的“控制通道”UDP 端口。
/*** @brief 启动 MJPEG 专用 HTTPS 服务器* @return httpd_handle_t 服务器句柄*/static httpd_handle_t start_mjpeg_webserver(void)
{httpd_handle_t server = NULL;ESP_LOGI(TAG, "Starting MJPEG webserver");/* MJPEG 专用 HTTPS 服务器配置 */httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();/* 仅启用 HTTPS,并改端口为 8443 */conf.transport_mode = HTTPD_SSL_TRANSPORT_SECURE; // 只跑 TLSconf.port_secure = 8443; // HTTPS 监听端口conf.httpd.ctrl_port = 32769;/* HTTPD 内部参数(仍在 conf.httpd.* 下设置) */conf.httpd.max_open_sockets = 3; // 流用 1~2 条,3 足够conf.httpd.lru_purge_enable = true; // 满员时踢最旧连接conf.httpd.task_priority = 5;conf.httpd.stack_size = 8192;/* 加载服务器证书 */extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start");extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end");conf.servercert = servercert_start;conf.servercert_len = servercert_end - servercert_start;/* 加载私钥 */extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");extern const unsigned char prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");conf.prvtkey_pem = prvtkey_pem_start;conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;/* 启动服务器 */esp_err_t ret = httpd_ssl_start(&server, &conf);if (ESP_OK != ret) {ESP_LOGE(TAG, "Error starting MJPEG server!");return NULL;}/* 注册 MJPEG URI 处理器 */ESP_LOGI(TAG, "Registering MJPEG URI handlers");httpd_register_uri_handler(server, &mjpeg_stream);return server;
}/*** @brief 停止 MJPEG 专用 HTTPS 服务器* @param server 服务器句柄* @return esp_err_t 停止结果*/
static esp_err_t stop_mjpeg_webserver(httpd_handle_t server)
{return httpd_ssl_stop(server);
}/*** @brief 启动 HTTPS 服务器* @return httpd_handle_t 服务器句柄*/
static httpd_handle_t start_webserver(void)
{httpd_handle_t server = NULL;ESP_LOGI(TAG, "Starting main HTTPS server");/* HTTPS 服务器配置 */httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();conf.port_secure = 443;conf.httpd.ctrl_port = 32768;conf.httpd.lru_purge_enable = true; // 启用最近最少使用清理机制/* 加载服务器证书 */extern const unsigned char servercert_start[] asm("_binary_servercert_pem_start");extern const unsigned char servercert_end[] asm("_binary_servercert_pem_end");conf.servercert = servercert_start;conf.servercert_len = servercert_end - servercert_start;/* 加载私钥 */extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");extern const unsigned char prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");conf.prvtkey_pem = prvtkey_pem_start;conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;/* 启动服务器 */esp_err_t ret = httpd_ssl_start(&server, &conf);if (ESP_OK != ret) {ESP_LOGE(TAG, "Error starting server!");return NULL;}/* 注册 URI 处理器 */ESP_LOGI(TAG, "Registering URI handlers");httpd_register_uri_handler(server, &root);httpd_register_uri_handler(server, &test_button);return server;
}/*** @brief 停止 HTTPS 服务器* @param server 服务器句柄* @return esp_err_t 停止结果*/
static esp_err_t stop_webserver(httpd_handle_t server)
{return httpd_ssl_stop(server);
}
参考上面的代码,我们启用了两个服务器,一个是start_mjpeg_webserver
一个是start_webserver
,分别用于视频流和主页面。
二者设置关键在这里:
/* 仅启用 HTTPS,并改端口为 8443 */
conf.transport_mode = HTTPD_SSL_TRANSPORT_SECURE; // 只跑 TLS
conf.port_secure = 8443; // HTTPS 监听端口
conf.httpd.ctrl_port = 32769;
必须设置不同的conf.port_secure
和conf.httpd.ctrl_port
,否则先启动的服务器会挤占通道资源,导致后面的服务器无法启动。
服务器注册与启动顺序
-
准备证书与 HTTPS 配置
对于https服务器需要配置密钥,参考启用WiFi和http server,对于需要开启https服务器的项目来说,推荐在main程序下新建certs文件夹,用来专门存在密钥对。推荐层级结构如下:esp32_https_server/├── main/│ ├── main.c # 主程序(包含所有功能)│ └── certs/ # SSL证书目录│ ├── servercert.pem # 服务器证书│ └── prvtkey.pem # 服务器私钥├── CMakeLists.txt # 构建配置└── sdkconfig # 项目配置
在certs目录下依次执行如下两条命令(需要先下载好openssl软件):
openssl genrsa -out prvtkey.pem 2048openssl req -new -x509 -key prvtkey.pem -out servercert.pem -days 365
执行第二个语句时需要填写一些内容:
Country Name (2 letter code):输入 CN State or Province Name:输入你的省份,比如 Beijing City Name:输入你的城市 Organization Name:输入你的组织名称 Organizational Unit Name:可以输入你的部门 Common Name:这个很重要,输入你的ESP32设备的IP地址或域名 Email Address:输入邮箱地址
如果有内容填就如实填,如果没有其实无所谓。需要稍微考虑一下的是Common Name这个选项,实测随便 填一个名字就可以。
生成并嵌入证书/私钥,填入
httpd_ssl_config_t
的servercert/servercert_len
、prvtkey_pem/prvtkey_len
;按需设置transport_mode
、port_secure
等。注意,要把需要文件在main目录层级下的CMakeLists.txt文件中引入,否则无法正确配置:idf_component_register(SRCS "main.c"INCLUDE_DIRS "."EMBED_TXTFILES "certs/servercert.pem" "certs/prvtkey.pem")
-
启动 HTTPS 服务器
在编写的服务器函数里调用httpd_ssl_start(&handle, &config)
创建服务器实例并分配资源,成功后获得 handle。例如上述代码中在每一个服务器注册函数的后半部分使用:/* 启动服务器 */ esp_err_t ret = httpd_ssl_start(&server, &conf); if (ESP_OK != ret) {ESP_LOGE(TAG, "Error starting server!");return NULL; }
-
定义 URL 路由与处理函数
路由用 httpd_uri_t 描述,包含 uri、method、handler 和可选 user_ctx;handler 内用 httpd_req_recv/httpd_resp_send 等处理请求。当前工程定义了三个路由,分别是页面根目录、视频流处理和一个测试按钮:/* URI 路由配置 */ static const httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler };static const httpd_uri_t mjpeg_stream = {.uri = "/stream",.method = HTTP_GET,.handler = mjpeg_stream_handler };static const httpd_uri_t test_button = {.uri = "/test-button",.method = HTTP_POST,.handler = test_button_handler };
-
注册路由到服务器
用httpd_register_uri_handler(handle, &uri)
进行绑定;若同 URI+方法已存在会返回ESP_ERR_HTTPD_HANDLER_EXISTS
。 -
(可选)启用高级功能
长连接、WebSocket(需 CONFIG_HTTPD_WS_SUPPORT)等按需启用,API 与 HTTP 服务器相同(少数底层传输覆盖 API 例外)。 -
停止服务器
调用httpd_ssl_stop(handle)
关闭 HTTPS 服务器并释放资源。本工程单独设计一个stop函数用来停止服务器,例如:/** * @brief 停止 MJPEG 专用 HTTPS 服务器* @param server 服务器句柄 * @return esp_err_t 停止结果*/ static esp_err_t stop_mjpeg_webserver(httpd_handle_t server) {return httpd_ssl_stop(server); }
以上配置完成,在app_main中依次启用服务器:
/* ========== HTTPS 服务器启动 ========== */
// 启动主服务器(端口443)
main_server = start_webserver();
if (main_server == NULL) {ESP_LOGE(TAG, "Failed to start main HTTPS server!");return;
}// 启动MJPEG专用服务器(端口8443)
mjpeg_server = start_mjpeg_webserver();
if (mjpeg_server == NULL) {ESP_LOGE(TAG, "Failed to start MJPEG HTTPS server!");// 主服务器仍可继续运行} else {ESP_LOGI(TAG, "MJPEG server started successfully on port 8443");
}