ESP32-C3 httpd 并发请求第二个必被 ECONNRESET,前端串行队列根治

一块 ESP32-C3 电源控制板,Web 前端通过 HTTP API 控制电源,WebSocket 推实时采样曲线。打开输出后刷新页面,输出按钮显示「已关闭」,实际输出保持开启。后端接口返回的状态正确,问题出在页面加载时前端同时发出多个 HTTP 请求:C3 的 httpd 只处理第一个,其余连接全部被重置。本文记录复现过程、原因和前端串行化修复。

现象:刷新后输出按钮显示关闭,输出实际开启

打开输出后刷新网页。输出按钮显示「已关闭」,电源输出实际保持开启。按钮状态没有同步到真实状态。

复现:并发 HTTP 请求第二个必被 ECONNRESET

先排除后端。用 curl 直查 /api/getSystemStatus,返回 is_output_on: 1,与实际状态一致。后端是实时查询 PSU,返回值正确。

再看前端。页面加载时同时执行三个接口请求(get_memory_preset、getSystemStatus、get_output_status),并建立 WebSocket 连接。用 node 脚本模拟这个行为:

结论:这台 C3 的 httpd 同时只处理一个请求,并发到达的其余连接被重置。

并发与串行请求对比

原因:单任务 httpd 加 PSU 慢查询占住处理循环

esp_http_server 是单任务模型。httpd 配置 max_open_sockets=7(C3 上限),lru_purge_enable=true。处理一个请求期间,并发到达的连接得不到处理,被 httpd 关闭,客户端收到 ECONNRESET。快速接口并发也失败,重置来自 httpd 单任务处理能力本身,与处理器快慢无关。

PSU 串口查询把问题放大。AST2/AST4/AST5 每条查询要约 1.5s(仪器把应答拆成多个小块、间隔约 300ms 吐出)。慢查询占住 httpd 任务时,并发到达的连接更明显被重置。

前端 .catch 把失败吞掉,界面停在初始值。getSystemStatus 失败后 isOutput 保持初始 false,按钮显示「已关闭」。

修复:前端请求队列串行化

让所有 HTTP 请求进同一个队列,前一个完成(成功或失败)再发下一个,任何时刻只有一个请求在飞。

var apiQueue=Promise.resolve();
function api(path, opts){
  var run=function(){
    return Promise.race([
      fetch(path, Object.assign({headers:{'Content-Type':'application/json'}}, opts||{}))
        .then(function(r){return r.json()}),
      new Promise(function(_,rej){setTimeout(function(){rej(new Error('timeout'))},10000)})
    ]).catch(function(e){showMsg('请求失败: '+path,'err'); throw e});
  };
  var p=apiQueue.then(run, run);
  apiQueue=p.catch(function(){});
  return p;
}

要点:

  1. apiQueue 保存前一条请求的完成状态,新的 api()apiQueue.then(run, run) 排队,队列空闲后才执行。
  2. apiQueue=p.catch(...) 吞掉错误,队列链不断,失败的请求不会卡住后续请求。
  3. 10s 超时兜底:PSU 查询挂死时队列不会被永久卡住。

WebSocket 握手也是 HTTP 连接。页面加载时先走完状态请求,队列清空后再 connectWS(),避免握手和 API 抢连接被重置:

syncStatus();
loadPreset();
apiQueue.then(connectWS);

加载顺序上先查 getSystemStatus,输出按钮在首个请求完成(约 1.6s)后即反映真实状态。串行加载三个查询全部成功,状态同步恢复。

边界:嵌入式前端的并发策略

串行化修复了这台 C3 的并发重置。前端不能假设设备有浏览器一样的并行请求能力,嵌入式 httpd 场景按这几条控制:

  1. 前端请求串行发送,任何时刻只允许一个请求在飞。慢查询占住 httpd 时并发请求全被重置。
  2. 合并接口、减少往返。多个状态能一次查询就一次返回,减少慢查询次数。
  3. 慢读取在后端做缓存或批量,让 httpd 处理器尽快返回,缩短占住单任务的时间。
  4. 实时数据走 WebSocket 批量推送(配合降采样),避免高频 HTTP 轮询挤占连接池。