3. 异步Web服务器基础:ESPAsyncWebServer库的安装、基本路由与响应

好,咱们进入正题。这一章要聊的是异步Web服务器,具体来说就是 ESPAsyncWebServer 这个库。你可能会问,ESP32 本身不是有 WiFiServer 吗?为什么还要多此一举?

嗯,这里有个关键点。传统的同步服务器,处理一个请求的时候,整个程序都得等着。如果这个请求要读 SPIFFS 里的一个大文件,或者要等某个传感器返回数据,那其他客户端就只能干瞪眼。这在 IoT 设备上是很要命的——你想想看,一个温湿度传感器页面加载慢,可能就把整个控制逻辑给拖垮了。

异步服务器就不一样了。它不阻塞。请求来了,它说“好,我知道了,你先等着,我处理完通知你”,然后扭头就去干别的事。这对 ESP32 这种资源有限的芯片来说,简直是救命的设计。

核心概念: 异步 ≠ 快,而是 不阻塞。它让服务器在等待 I/O 操作时,能继续处理其他请求。

3.1 安装 ESPAsyncWebServer 库

安装这个库,我建议你别用 Arduino 库管理器直接搜。为什么?因为版本太乱,而且依赖关系经常搞错。我个人习惯是手动安装,一劳永逸。

你需要两个库:

  1. ESPAsyncWebServer:核心库,负责处理 HTTP 请求和响应。
  2. AsyncTCP:底层 TCP 库,ESPAsyncWebServer 依赖它。注意,这是针对 ESP32 的版本,ESP8266 用的是 ESPAsyncTCP,别搞混了。

安装步骤很简单:

  • 去 GitHub 上把这两个库的源码下载下来(ZIP 包)。
  • 在 Arduino IDE 里,选择 项目 → 加载库 → 添加 .ZIP 库,先装 AsyncTCP,再装 ESPAsyncWebServer。
  • 装完后,在 文件 → 示例 里如果能找到 ESPAsyncWebServer 的示例,就说明成功了。

我曾经踩过的坑: 有一次我直接用了库管理器里的版本,结果编译报错,提示找不到 AsyncTCP.h。折腾了半天才发现,库管理器里的 ESPAsyncWebServer 版本太新,而 AsyncTCP 版本没跟上。从那以后,我都是手动从 GitHub 的 Release 页面下载,确保两个库的发布时间相差不超过一个月。

3.2 第一个异步服务器:Hello World

装好了,咱们写个最简单的例子。让 ESP32 变成一个 Web 服务器,访问根路径时返回 "Hello, ESP32!"。

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";

AsyncWebServer server(80);  // 创建服务器对象,监听80端口

void setup() {
  Serial.begin(115200);
  
  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("正在连接WiFi...");
  }
  Serial.println("WiFi已连接");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());

  // 定义路由
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello, ESP32!");
  });

  // 启动服务器
  server.begin();
  Serial.println("HTTP服务器已启动");
}

void loop() {
  // 啥也不干,异步服务器自己处理
}

这段代码里,最核心的就是 server.on() 这个函数。它定义了一个路由:当客户端用 GET 方法访问 "/" 路径时,执行后面的回调函数。回调函数里,我们用 request->send() 返回数据。

注意看 loop() 函数——它是空的。这就是异步的魅力:你不需要在 loop 里调用任何处理函数,服务器自己会在后台处理所有请求。

3.3 路由的几种写法

实际项目中,路由不会只有一条。ESPAsyncWebServer 支持多种路由定义方式,我给大家梳理一下:

路由方式 代码示例 说明
固定路径 server.on("/led", HTTP_GET, handler); 精确匹配 "/led" 路径
路径参数 server.on("/sensor/{id}", HTTP_GET, handler); 匹配 "/sensor/1"、"/sensor/abc" 等,用 request->pathArg("id") 获取参数
通配符 server.on("/api/*", HTTP_GET, handler); 匹配 "/api/xxx"、"/api/xxx/yyy" 等任意子路径
多方法 server.on("/data", HTTP_GET | HTTP_POST, handler); 同时处理 GET 和 POST 请求

我的建议: 路径参数的方式最灵活,但要注意参数名不要和系统保留字冲突。我在一个项目里用过 "type" 作为参数名,结果和某些浏览器的预检请求冲突了,排查了半天。后来统一改成 "kind" 就没事了。

3.4 响应类型:不只是纯文本

request->send() 函数有很多重载版本,可以返回不同类型的数据。咱们看看常用的几种:

// 返回纯文本
request->send(200, "text/plain", "OK");

// 返回 HTML
request->send(200, "text/html", "<h1>Hello</h1>");

// 返回 JSON
request->send(200, "application/json", "{\"status\":\"ok\"}");

// 返回状态码(无内容)
request->send(404);  // 文件未找到

// 重定向
request->redirect("/login");

这里有个细节:MIME 类型(第二个参数)一定要写对。如果你返回 HTML 却写了 "text/plain",浏览器会把它当成纯文本显示,不会渲染标签。我刚开始学的时候犯过这个错,页面显示了一堆尖括号,还以为是 ESP32 坏了。

3.5 处理 POST 请求和表单数据

光有 GET 不够,很多时候你需要接收客户端发来的数据,比如配置 WiFi、控制设备。这时候就要用 POST 了。

server.on("/submit", HTTP_POST, [](AsyncWebServerRequest *request){
  // 获取表单参数
  String name = request->arg("name");
  String value = request->arg("value");
  
  Serial.print("收到参数 - name: ");
  Serial.print(name);
  Serial.print(", value: ");
  Serial.println(value);
  
  // 处理数据...
  
  request->send(200, "text/plain", "数据已接收");
});

如果客户端上传的是 JSON 数据,可以用 request->hasParam("plain", true) 来获取原始请求体:

server.on("/api/data", HTTP_POST, [](AsyncWebServerRequest *request){
  if (request->hasParam("plain", true)) {
    String body = request->getParam("plain", true)->value();
    Serial.println("收到JSON: " + body);
    // 这里可以用 ArduinoJson 库解析 body
  }
  request->send(200, "application/json", "{\"received\":true}");
});

3.6 异步服务器的核心流程图

说了这么多,咱们用一张图来总结异步服务器的处理流程。这张图我画了很多遍,每次给团队新人培训都用它。

ESPAsyncWebServer 请求处理流程 客户端 HTTP 请求 服务器入口 路由匹配 回调处理 响应发送 不阻塞,继续处理 关键点: • 客户端发起请求后,服务器立即返回“已接收”,不等待处理完成 • 路由匹配和回调处理在后台进行,不影响其他请求的接收 • 响应发送完成后,服务器自动释放资源,准备处理下一个请求

3.7 常见问题与调试技巧

最后,分享几个我在实际调试中遇到的问题,希望能帮你少走弯路。

问题1:服务器启动后,浏览器访问超时

原因:WiFi 没连上,或者 IP 地址输错了。我建议在串口监视器里打印出 WiFi.localIP(),然后用手机或电脑 ping 一下这个地址,确认网络通不通。

问题2:路由匹配不上,总是返回 404

原因:路径写错了,或者大小写不匹配。ESPAsyncWebServer 默认是大小写敏感的。比如你定义了 /LED,但浏览器访问 /led,就会 404。解决办法:要么统一用小写,要么在路由定义时用 server.on("/led", HTTP_GET, handler); 并确保客户端也用小写。

问题3:回调函数里操作了全局变量,导致数据错乱

原因:异步回调是在不同时间片执行的,如果多个请求同时修改同一个全局变量,就会产生竞态条件。我曾经在一个项目里用全局变量记录 LED 状态,结果两个浏览器窗口同时操作,灯闪得跟迪斯科一样。解决办法:用 volatile 关键字声明变量,或者加互斥锁(虽然 ESP32 上不常用,但心里要有这根弦)。