4、HTTP服务器搭建:创建ESP32 Web服务器、处理路由、返回HTML页面

好,到了这一章,我们终于要让ESP32真正“开口说话”了。

前面几章我们把开发环境搭好、Wi-Fi连上、基本通信搞通,但说实话,那都是热身。从这章开始,ESP32会变成一个真正的Web服务器——你打开浏览器,输入它的IP地址,就能看到它返回的页面。这才是前端交互的第一步。

4.1 为什么要在ESP32上搭HTTP服务器?

你可能要问:ESP32这么小的MCU,跑HTTP服务器靠谱吗?

我刚开始做这个决定时也有点犹豫。但后来发现,ESP32的双核240MHz处理器加上4MB Flash,跑一个轻量级的HTTP服务器绰绰有余。说白了,我们不是要让它处理百万并发,而是让它能响应几个客户端的请求——这完全没问题。

在嵌入式设备上搭建HTTP服务器,有几个实实在在的好处:

  • 零客户端依赖:任何设备只要有浏览器,就能访问ESP32的页面
  • 调试方便:我在项目里经常用手机浏览器直接看传感器数据,不用装任何App
  • 跨平台:Windows、macOS、Linux、Android、iOS,通吃
  • 为WebSocket打基础:HTTP是WebSocket的“敲门砖”,握手阶段必须走HTTP
我的习惯:在正式做WebSocket项目前,我都会先搭一个HTTP服务器做“冒烟测试”。如果HTTP页面能正常返回,说明网络栈和Wi-Fi都没问题,后面WebSocket的调试会省很多时间。

4.2 ESP32上的HTTP服务器方案选型

ESP32的官方SDK(ESP-IDF)里自带了一个HTTP服务器组件,叫esp_http_server。如果你用Arduino框架,也有对应的库。我个人更推荐用ESP-IDF原生方案,原因很简单——稳定、可控、文档全。

来看一下几个主流方案的对比:

方案 框架 内存占用 性能 适用场景
esp_http_server ESP-IDF 低 (~30KB) 高 生产级项目
AsyncWebServer Arduino 中 (~50KB) 中 快速原型
WebServer库 Arduino 低 (~25KB) 低 简单测试

嗯,这里要注意:如果你用Arduino的WebServer库,它是同步阻塞的——一个请求没处理完,其他请求就得排队等着。这在生产环境里是个大坑。我踩过这个坑,后来全换成esp_http_server了。

4.3 核心概念:URI、路由、Handler

在动手写代码之前,我们先理清三个概念。搞懂了它们,后面写代码就是填空。

  • URI(统一资源标识符):就是浏览器地址栏里“/”后面的那串东西。比如/、/sensor、/style.css
  • 路由(Route):把URI和对应的处理函数绑定起来。比如“当用户访问/时,执行root_get_handler”
  • Handler(处理函数):真正干活儿的函数。它接收请求,生成响应,然后返回给客户端

说白了,路由就是“门牌号”,Handler就是“屋里的人”。你敲哪个门,对应的人出来应答。

核心流程:客户端发起HTTP请求 → 服务器解析URI → 匹配路由表 → 调用对应的Handler → Handler生成响应 → 服务器发送响应给客户端

4.4 实战:搭建第一个ESP32 Web服务器

好,我们直接上代码。下面这个例子会创建一个HTTP服务器,监听80端口,处理两个路由:/返回一个简单的HTML页面,/api/hello返回JSON数据。

#include <esp_http_server.h>
#include <esp_log.h>

static const char *TAG = "HTTP_SERVER";

/* Handler for GET / */
static esp_err_t root_get_handler(httpd_req_t *req)
{
    const char resp[] = "<!DOCTYPE html><html><body>"
                        "<h1>Hello from ESP32!</h1>"
                        "<p>This is my first Web page.</p>"
                        "</body></html>";
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

/* Handler for GET /api/hello */
static esp_err_t hello_get_handler(httpd_req_t *req)
{
    const char resp[] = "{\"message\":\"Hello from ESP32 API\"}";
    httpd_resp_set_type(req, "application/json");
    httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

/* Start the HTTP server */
httpd_handle_t start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = 80;
    config.max_uri_handlers = 10;

    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) != ESP_OK) {
        ESP_LOGE(TAG, "Failed to start server");
        return NULL;
    }

    /* Register URI handlers */
    httpd_uri_t uri_root = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = root_get_handler,
        .user_ctx  = NULL
    };
    httpd_register_uri_handler(server, &uri_root);

    httpd_uri_t uri_hello = {
        .uri       = "/api/hello",
        .method    = HTTP_GET,
        .handler   = hello_get_handler,
        .user_ctx  = NULL
    };
    httpd_register_uri_handler(server, &uri_hello);

    ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
    return server;
}
我曾经踩过的坑:一开始我把httpd_resp_set_type写成了httpd_resp_set_status,结果浏览器一直显示乱码。后来才发现,Content-Type没设对,浏览器不知道该怎么解析返回的数据。记住:HTML页面要设text/html,JSON要设application/json,CSS要设text/css。

4.5 返回完整的HTML页面

上面那个例子返回的HTML太简陋了。实际项目中,我们需要返回完整的页面——带CSS样式、带JavaScript脚本、甚至带图片。但ESP32的Flash空间有限,不能像PC服务器那样放几百个文件。怎么办?

我常用的做法有两种:

  1. 内嵌HTML:把整个页面写成字符串,直接编译进固件。适合小页面(几KB以内)
  2. SPIFFS文件系统:把HTML/CSS/JS文件上传到ESP32的Flash文件系统里,服务器从文件系统读取并返回。适合大页面

这一章我们先讲第一种——内嵌HTML。后面会有专门章节讲SPIFFS。

来看一个带样式的完整页面例子:

static const char* index_html = 
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>ESP32 Dashboard</title>"
"<style>"
"body { font-family: Arial; text-align: center; margin-top: 50px; background: #f0f8ff; }"
"h1 { color: #2c3e50; }"
".card { background: white; padding: 20px; margin: 20px auto; max-width: 400px; "
"        border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }"
"</style>"
"</head>"
"<body>"
"<h1>ESP32 Dashboard</h1>"
"<div class=\"card\">"
"<h2>System Info</h2>"
"<p>Chip: ESP32</p>"
"<p>Free Heap: ";

/* 在handler里拼接动态数据 */
static esp_err_t dashboard_get_handler(httpd_req_t *req)
{
    char buf[1024];
    int heap = esp_get_free_heap_size();
    
    snprintf(buf, sizeof(buf), 
        "%s%d bytes</p></div></body></html>",
        index_html, heap);
    
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, buf, strlen(buf));
    return ESP_OK;
}

你看,我把静态的HTML模板拆成两部分:前半段是固定的样式和结构,后半段在handler里用snprintf把动态数据(比如空闲堆内存)拼进去。这样既保持了代码整洁,又能返回动态内容。

4.6 本章知识结构图

下面这张图把HTTP服务器的核心流程和本章涉及的关键点串起来了:

ESP32 HTTP服务器核心流程 浏览器/客户端 HTTP请求 路由匹配 URI → Handler Handler HTTP响应 浏览器渲染 关键点 URI路由表:/ → root_get_handler, /api/hello → hello_get_handler 响应类型:HTML用text/html,JSON用application/json 路由表示例 GET / → root_get_handler 返回: HTML页面 GET /api/hello → hello_get_handler 返回: JSON数据

4.7 常见问题与避坑指南

写HTTP服务器时,有几个问题我几乎每次都会遇到。列出来给你参考:

  • 端口被占用:如果Wi-Fi没连上就启动服务器,80端口可能被其他服务占用。我习惯在启动服务器前先检查Wi-Fi状态
  • 内存不足:每个HTTP连接会占用约4KB的RAM。如果并发连接太多,ESP32可能崩溃。设置max_open_sockets为5就够用了
  • Content-Type设错:浏览器是根据Content-Type来解析响应的。设错了,页面可能显示乱码或空白
  • 字符串太长:内嵌HTML时,字符串长度不要超过ESP32的栈空间。我一般把大字符串声明为static const,放在Flash里
调试小技巧:在浏览器里按F12打开开发者工具,切换到Network标签页。刷新页面,可以看到每个请求的详细信息——包括请求头、响应头、状态码、响应内容。这是排查HTTP问题最直接的方法。

4.8 小结

这一章我们做了三件事:理解了HTTP服务器的核心概念、用esp_http_server搭建了第一个服务器、学会了返回带样式的HTML页面。你现在已经能让ESP32“说话”了——虽然只是简单的“Hello World”,但这是后面所有交互的基础。