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 脚本模拟这个行为:
- 同时发两个请求,第一个 200,第二个报
read ECONNRESET;连测两次结果一致。 - 同时发三个快速接口(get_config、debug.json、network.json),也只成功第一个。
- 把请求改成串行,三个全部成功(getSystemStatus 1559ms、get_output_status 1904ms、get_memory_preset 920ms)。
结论:这台 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;
}要点:
apiQueue保存前一条请求的完成状态,新的api()用apiQueue.then(run, run)排队,队列空闲后才执行。apiQueue=p.catch(...)吞掉错误,队列链不断,失败的请求不会卡住后续请求。- 10s 超时兜底:PSU 查询挂死时队列不会被永久卡住。
WebSocket 握手也是 HTTP 连接。页面加载时先走完状态请求,队列清空后再 connectWS(),避免握手和 API 抢连接被重置:
syncStatus();
loadPreset();
apiQueue.then(connectWS);加载顺序上先查 getSystemStatus,输出按钮在首个请求完成(约 1.6s)后即反映真实状态。串行加载三个查询全部成功,状态同步恢复。
边界:嵌入式前端的并发策略
串行化修复了这台 C3 的并发重置。前端不能假设设备有浏览器一样的并行请求能力,嵌入式 httpd 场景按这几条控制:
- 前端请求串行发送,任何时刻只允许一个请求在飞。慢查询占住 httpd 时并发请求全被重置。
- 合并接口、减少往返。多个状态能一次查询就一次返回,减少慢查询次数。
- 慢读取在后端做缓存或批量,让 httpd 处理器尽快返回,缩短占住单任务的时间。
- 实时数据走 WebSocket 批量推送(配合降采样),避免高频 HTTP 轮询挤占连接池。