1006是客户端在tcp无声断连后自动生成的兜底标识,非服务端发送;因nginx/node.js/tomcat/nat等任一环节超时早于心跳响应,触发rst导致客户端收不到任何帧,故总在心跳后出现,需统一调大各层超时并校验upgrade头与连接状态。

1006 不是服务端发的错误码,而是客户端在 TCP 连接无声断开后“自动生成”的兜底标识;查服务端日志找不到它,重连逻辑不检查 event.wasClean === false 就等于没防住。
为什么 1006 总在心跳之后出现?
因为心跳本身没用——只要中间某层(Nginx、Node.js http.Server、Tomcat、运营商 NAT)先于心跳响应超时,就会直接 RST 断连,客户端收不到任何帧,只能报 1006。
- Nginx 默认
proxy_read_timeout是 60 秒,你设 30 秒心跳,第二个 pong 永远到不了浏览器 - Node.js 的
http.Server在noServer: true模式下仍会施加默认timeout(60 秒)和headersTimeout(也常为 60 秒),与 WebSocket 协议无关 - Android 定制 ROM 或 iOS Safari 切后台,系统可能 30–90 秒内强制回收 socket,不发 close 帧
- 客户端发消息前不检查
ws.readyState !== WebSocket.OPEN,就会在已断未觉状态下触发 1006
Nginx 代理必须配齐的三样东西
少一个,WebSocket 就大概率在 60 秒后静默死亡。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
proxy_http_version 1.1:HTTP/1.0 不支持 upgrade -
proxy_set_header Upgrade $http_upgrade:透传 Upgrade 头,否则握手失败 -
proxy_set_header Connection "upgrade":显式声明升级意图,不能写成"Upgrade"或漏引号 - 必须同时调大
proxy_read_timeout和proxy_send_timeout,建议 ≥ 300(5 分钟),且要大于心跳间隔 × 2
Node.js ws 库 + 手动 handleUpgrade 的陷阱
用 noServer: true 却不监听 server.on('upgrade'),等于把 WebSocket 连接交给一个还在执行 HTTP 超时逻辑的 server 管理。
- 错误做法:创建
http.Server→ 调wss.handleUpgrade()→ 完事。此时 server 仍会 60 秒后destroysocket - 正确做法一(推荐):
new WebSocket.Server({ server }),它会自动禁用http.Server的超时 - 正确做法二:显式
server.on('upgrade', (req, socket, head) => { wss.handleUpgrade(req, socket, head, cb) }) - HTTPS 场景下,
server必须是https.Server实例,不是裸net.Socket
客户端重连前最容易被忽略的两件事
1006 频繁发生,往往不是因为连不上,而是因为“连上了却以为没连上”或“连着却发不出消息”。
- 不要等
onclose再重连——要在每次send()前检查ws.readyState === WebSocket.OPEN,否则弱网切换后第一次发消息就崩出 1006 - 主动
close()前必须清除所有定时器(心跳、重连、超时),否则残留setInterval可能干扰新连接的状态判断 -
event.reason几乎总是空字符串,别依赖它;event.wasClean === false才是识别 1006 的可靠依据 - Chrome DevTools 的 Network → WS → Frames 标签页里,看最后是否卡在 ping 后无 pong,比看控制台红字更准
真正的问题从来不在 1006 这个数字上,而在于你是否清楚整个链路上每个组件的超时策略——Nginx、容器(Tomcat/Undertow)、服务框架、移动端系统、甚至运营商 NAT 设备,它们都在默默倒计时。










