TCP 连接预算
TCP 连接预算
FastBee Arduino设备端 Web 服务器基于 ESPAsyncWebServer + AsyncTCP,其并发能力受 lwIP TCP PCB 表 限制。 各芯片的资源禀赋不同,连接预算需差异化配置,否则会导致页面打不开或 SSE 推送丢失。
连接分配遵循以下原则:
TCP_TOTAL = SSE(持久推送)+ HTTP(页面/接口请求)- SSE:保留槽位给
AsyncEventSource(状态实时推送) - HTTP:剩余槽位供页面加载和 API 调用并发使用
- 预算常量:统一定义在
include/core/ResourceProfile.h - SSE 上限:
SSERouteHandler.h中MAX_SSE_CLIENTS固定数组控制 - TCP 耗尽阈值:
WebConfigManager.h中TCP_CONN_EXHAUSTION_THRESHOLD控制软重启触发点
注意:AsyncTCP v3.4.10 已不再读取
CONFIG_ASYNC_TCP_MAX_CONNECTIONS宏,该标志为遗留配置。 真正的连接数由 lwIPMEMP_NUM_TCP_PCB(默认 16)和业务层阈值共同决定。
各芯片硬件参数
| 芯片 | 内核 | 内部 SRAM | PSRAM | WiFi |
|---|---|---|---|---|
| ESP32 | 双核 Xtensa 240MHz | 520 KB | 无 | 802.11 b/g/n |
| ESP32-S3 | 双核 Xtensa 240MHz | 512 KB | 8 MB | 802.11 b/g/n |
| ESP32-C6 | 单核 RISC-V 160MHz | 512 KB | 无 | 802.11 ax (Wi-Fi 6) |
| ESP32-C3 | 单核 RISC-V 160MHz | 400 KB | 无 | 802.11 b/g/n |
内存预算计算
lwIP 默认配置(所有 ESP32 变体相同,来自 ESP-IDF sdkconfig):
| 参数 | 默认值 | 说明 |
|---|---|---|
MEMP_NUM_TCP_PCB | 16 | lwIP TCP PCB 硬上限(所有变体,不可突破) |
TCP_SND_BUF | 5744 B | 每连接发送缓冲 |
TCP_WND | 5760 B | 每连接接收窗口 |
| TCP PCB 结构体 | ~172 B | lwIP 内部控制块 |
| 每连接合计 | ~11.7 KB | PCB + SND_BUF + WND |
单用户访问场景分析(浏览器单 host 最多 6 并发):
| 状态 | MQTT | SSE | HTTP | TIME_WAIT | 总 PCB |
|---|---|---|---|---|---|
| 页面加载峰值 | 1 | 1 | 5-6 | 2-4 | 9-12 |
| 稳态 | 1 | 1 | 0-2 | 1-2 | 3-6 |
| 多标签页(S3) | 1 | 2 | 4-6 | 3-5 | 10-14 |
各芯片连接预算
| 芯片 | TCP 预算 | SSE | HTTP | 耗尽阈值 | 状态 |
|---|---|---|---|---|---|
| ESP32 classic | 6 | 1 | 5 | 12 | ✅ 已实施 |
| ESP32-S3 | 8 | 2 | 6 | 14 | ✅ 已实施 |
| ESP32-C6 | 6 | 1 | 5 | 12 | ✅ 已实施 |
| ESP32-C3 | 4 | 1 | 3 | 10 | ✅ 已实施 |
耗尽阈值必须 < 16(lwIP 硬上限),在接近上限前触发恢复,留 2-6 个槽位缓冲。 内存紧张的芯片(C3)更早触发,内存充裕的(S3)容忍更多连接。
PSRAM 对 TCP 连接承载力的影响
ESP32-S3 配 PSRAM 后,内部 DRAM 竞争压力大幅缓解,间接支撑更多并发 TCP 连接:
| 场景 | 无 PSRAM | 有 PSRAM(阈值=512B) |
|---|---|---|
| 内部 DRAM 可用 | ~18 KB | ~22-35 KB(HTTP 缓冲卸载到 PSRAM) |
| lwIP TCP PCB 空间 | 紧张,易耗尽 | 充裕(每个 PCB ~172B,必须内部 DRAM) |
| 并发 HTTP 请求 | 2-3 个即触发内存压力 | 6+ 个仍可正常处理 |
CONFIG_ASYNC_TCP_MAX_CONNECTIONS=14(S3)是构建标志文档,AsyncTCP v3.4.10 已不再读取。 实际连接数受 lwIPMEMP_NUM_TCP_PCB=16和业务层TCP_TOTAL_BUDGET=8共同约束。 提升该标志的主要目的是作为配置意图文档,以及向后兼容旧版 AsyncTCP。
关键文件
| 文件 | 作用 |
|---|---|
include/core/ResourceProfile.h | TCP_TOTAL_BUDGET / TCP_SSE_BUDGET / TCP_HTTP_BUDGET 预算常量(按芯片条件编译) |
include/network/handlers/SSERouteHandler.h | MAX_SSE_CLIENTS 固定数组槽位数 |
include/network/WebConfigManager.h | TCP_CONN_EXHAUSTION_THRESHOLD 软重启阈值;CHECK_INTERVAL_MS / TIME_WAIT_PRUNE_* 修剪参数;NO_REQUEST_WATCHDOG_MS 无请求看门狗 |
src/network/WebConfigManager.cpp | 每 30s 主动 abort TIME_WAIT 连接 + TIME_WAIT 超阈修剪 + 无请求看门狗 + 耗尽检测与自动恢复 |
src/network/WebHandlerContext.cpp | 静态文件并发限流:STATIC_MAX_INFLIGHT / STATIC_INFLIGHT_WATCHDOG_MS / 小资源豁免 / 503 自动重试页 |
web-src/index.html | 前端 boot chunk 串行加载器:间隔/启动延迟/退避重试参数 |
platformio.ini | 各 [xxx_runtime_flags] 中 CONFIG_ASYNC_TCP_MAX_CONNECTIONS(遗留标志,AsyncTCP 3.4.10 不再读取,S3 已提升至 14) |
治理措施
TIME_WAIT 连接定期清理
ESPAsyncWebServer 关闭连接后,TCP PCB 进入 TIME_WAIT 状态(默认持续 2×MSL ≈ 60s), 期间仍占用槽位。WebConfigManager 每 30 秒遍历 lwIP PCB 链表,主动 abort 超时的 TIME_WAIT 连接:
// WebConfigManager.cpp — 每 30s 清理一次 TIME_WAIT
if (millis() - lastTimeWaitCleanup > 30000) {
lastTimeWaitCleanup = millis();
// abort TIME_WAIT connections to free TCP PCB slots
}TIME_WAIT 主动修剪
Web 响应强制 Connection: close 短连接,密集访问时每个请求都会产生一个 TIME_WAIT PCB(lwIP 持续 2×MSL ≈ 120s),很快占满 MEMP_NUM_TCP_PCB=16 导致新请求挂起。除上述 30s 周期清理外,WebConfigManager 每 2s 做一次 TCP 健康监测,TIME_WAIT 超过阈值时立即修剪:
| 参数 | 值 | 说明 |
|---|---|---|
CHECK_INTERVAL_MS | 2000ms | TCP 健康检查间隔 |
TIME_WAIT_PRUNE_THRESHOLD | 4 | TIME_WAIT 超过此值开始修剪 |
TIME_WAIT_PRUNE_KEEP | 1 | 修剪后保留数量 |
修剪只杀最老的 TIME_WAIT(与 lwIP tcp_kill_timewait 同策略),该状态下 tcp_abort 直接移除 PCB 不发 RST,对已关闭连接无副作用。
无请求看门狗与 SSE 保护
isRunning=true 但连续 120s(NO_REQUEST_WATCHDOG_MS)未收到任何 HTTP 请求且存在残留 TCP 连接时,看门狗会就地回收端口 80 上的僵死 PCB(不做风险更高的软重启)。两条保护规则防止误杀:
- SSE 客户端在线时跳过回收:SSE 是合法长连接且不计入 HTTP 请求,长时间无 HTTP 请求属正常现象,abort 会切断浏览器进行中的请求链
- 认证请求统一记录活动:大量处理器经由
HandlerUtils的 chunked/stream 路径发送响应,那些路径不更新lastRequestSeenMs,因此requireAuth()成功后统一调用noteWebRequestActivity(),避免看门狗误判“无人访问”
规则查询类接口在 async_tcp 任务上持锁读取规则表,必须使用有界等待(MutexGuard 显式超时,如 2000ms)并在超时后返回 503,禁止默认 portMAX_DELAY 无限等待——规则执行长时间占锁时无限等待会冻结所有 HTTP 请求。
SSE 连接数限制
SSERouteHandler 使用固定数组 _slots[MAX_SSE_CLIENTS] 跟踪客户端。 ESP32-S3 设 MAX_SSE_CLIENTS=2(支持多标签页),其余芯片设 MAX_SSE_CLIENTS=1(单 SSE 连接)。 超出槽位时拒绝新连接,防止 SSE 占满 TCP PCB 导致 HTTP 请求无法进入。
静态文件并发限流
浏览器首次导航会突发 ~12 个资源请求(HTML 文档 + JS chunk + CSS + 图片 + API), 而每个响应都强制 Connection: close,资源只能串行/小并发排队。 历史上曾出现两个方向的故障:
- 并发过高:4 路并发流式传输 128KB CSS 把 DRAM 打穿(最大连续块降到 220B),
operator new失败 abort() 导致设备崩溃循环 - 限流过严:上限设为 1 时,首屏突发请求只放行 1 个,其余被 RST/超时或 503 "Static file busy" 拒绝,主文档被拒时浏览器直接停留在 JSON 白屏
当前方案(WebHandlerContext.cpp)在两者之间取平衡:
| 参数 | 值 | 说明 |
|---|---|---|
STATIC_MAX_INFLIGHT | 2 | 可压缩资源(html/js/css)同时在途上限;当前资源均 ≤22KB gz,2 路并发在 36KB DRAM 门控内安全 |
STATIC_INFLIGHT_WATCHDOG_MS | 12000 | 计数卡死看门狗:onDisconnect 未触发导致计数器泄漏时,12s 后强制复位,避免永久 503 |
| 小资源豁免 | favicon/logo 等未压缩资源 | bypassInFlightCap = !isCompressible,单文件仅几 KB 不构成历史崩溃场景(大文件并发流),首屏必需资源不再被拒 |
| 槽位释放 | onDisconnect | Connection: close 保证完成/超时/abort 都会触发释放 |
被拒绝时的响应策略按资源重要性区分,避免 503 白屏:
| 请求类型 | 拒绝响应 | 恢复方式 |
|---|---|---|
核心入口文档(/www/index.html、/www/setup.html 含 gz) | 503 + text/html 自动重试页(<meta http-equiv="refresh" content="1">) | 浏览器每秒自动重新导航直至恢复 |
| 普通静态资源 | 503 + JSON(retryAfter: 1) | 前端 loader onerror 递增退避重试 |
前端加载器(web-src/index.html)与服务端限流配套的时序参数:
| 参数 | 当前值 | 旧值 | 说明 |
|---|---|---|---|
| boot chunk 间隔 | 30ms | 80ms | 5 个 chunk 串行加载,间隔直接叠加到首屏时间 |
| 启动延迟 | 20ms | 100ms | DOMContentLoaded 后开始加载 |
| 失败重试退避 | 300/1200/2700ms | 500/2000/4500ms | 300×attempt²,最多 4 次(CHUNK_MAX_RETRY) |
修复后实测(ESP32-F4R0,登录页约 12 资源):登录表单 ~2.8s 完整可见(DOM load≈2763ms), 达成 3 秒目标;主文档被限流时不再白屏,自动重试页每秒恢复探测。
修改注意事项:
STATIC_MAX_INFLIGHT提升前必须确认最大 gz 资源体积 × 并发数仍在 36KB DRAM 门控内; 曾实测 4 路 128KB 并发直接崩溃循环,不要盲目放开- 看门狗值必须大于单个文件的合法传输时长(弱网下可达数秒),否则正常传输会被误杀
- 核心入口拒绝路径(DRAM 门控 2 处 + 限流 1 处)必须全部走
sendStaticBusyRetryPage(), 回退到 JSON 会重现 503 白屏;以上常量与行为由test/test_static_concurrency.cpp锁定, 修改后需跑pio test -e native回归 - 前端 loader 时序参数同样被
test_static_concurrency.cpp锁定;调慢退避前先用 并发探测脚本复测设备实际拒绝率
修改注意事项
- 任何阈值修改必须保证
TCP_CONN_EXHAUSTION_THRESHOLD < 16(lwIP PCB 硬上限) - 修改静态限流常量(
STATIC_MAX_INFLIGHT/STATIC_INFLIGHT_WATCHDOG_MS)或前端 loader 时序参数后,必须同步更新并回归test/test_static_concurrency.cpp的锁定断言 - 调整
TIME_WAIT_PRUNE_THRESHOLD/TIME_WAIT_PRUNE_KEEP/CHECK_INTERVAL_MS时,同步检查test_mqtt_protocol.cpp的看门狗常量回归断言 - 无请求看门狗的 reap 逻辑修改后,必须保留 SSE 在线跳过分支(
test_mqtt_protocol.cpp已锁定) - 修改
ResourceProfile.h中的TCP_TOTAL_BUDGET后,需同步更新:SSERouteHandler.h中的MAX_SSE_CLIENTS(≤TCP_SSE_BUDGET)WebConfigManager.h中的TCP_CONN_EXHAUSTION_THRESHOLDplatformio.ini各[xxx_runtime_flags]中的CONFIG_ASYNC_TCP_MAX_CONNECTIONSscripts/validate-build-matrix.js中的期望值
- ESP32-C3 RAM 最小(400KB),TCP 不宜超过 4
CONFIG_ASYNC_TCP_MAX_CONNECTIONS已被 AsyncTCP 3.4.10 废弃,但保留作为配置文档和向后兼容
