1006 错误是TCP层异常关闭所致,非代码逻辑错误;根本原因包括Node.js http.Server默认60秒超时、noServer:true模式下socket未被接管、Nginx proxy_read_timeout配置不当,以及心跳响应闭环缺失。
1006 不是代码写错了,而是连接在底层被“悄无声息”掐断了——它不报错、不触发 onerror、reason 通常为空,只留下一个 wasclean: false。
为什么 1006 总在 60 秒左右出现
这不是巧合。Node.js 的 http.Server 默认 timeout 和 headersTimeout 都是 60 秒。当你用 ws 库的 noServer: true 模式 + 手动 handleUpgrade,HTTP 服务器仍会为每个 upgrade socket 设置超时,但不会转发心跳帧,导致 TCP 连接被内核直接 RST。
- 服务端若用
new WebSocket.Server({ server })初始化,ws会自动禁用相关超时,这是最稳妥的写法 - 若必须手动处理 upgrade(比如集成 Express),需显式设置
req.socket.setTimeout(0)和server.timeout = 0 - Nginx 默认
proxy_read_timeout 60,必须同步调大,否则它会在无数据时主动断开 TCP,不发 close 帧
onclose 里只看 event.code 是不够的
CloseEvent 有三个关键字段:code、reason、wasClean。仅靠 code === 1006 只能确认“异常”,但无法区分是客户端休眠、Wi-Fi 切换,还是代理静默断连。
-
wasClean === false是1006的铁律,但reason在 Chrome/Firefox 中多数为空;Safari 和部分 CDN 可能填入字符串(如"connection closed before receiving a response") - 不要在
onclose里直接重连——先检查ws.readyState是否为CLOSED,避免重复创建已关闭实例 - 对
4000–4999自定义码,应停止重连并提示用户;1001(going away)可能是页面卸载,此时不应重连
心跳不是“发 ping 就完事”,关键在响应闭环
前端每 30 秒 send('ping'),不代表服务端一定收到了、更不代表它回应了 pong。很多 1006 实际源于服务端未正确处理 ping 帧,或 pong 响应延迟超出了代理/客户端容忍阈值。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- Node.js
ws库需手动开启:设置pingInterval并监听pong事件,否则不会自动回复 - Spring WebSocket 需配置
setHeartbeatValue(new Duration(10, TimeUnit.SECONDS)),否则默认不发心跳 - 客户端应记录最近一次收到
pong的时间戳,连续 2 次超时(如 90 秒没响应)才判定为断连,避免因单次网络抖动误判
抓包比 console.log 更早看到真相
Chrome DevTools 的 WS Frames 面板只能看到应用层帧,而 1006 往往发生在 TCP 层——你看到的“最后一条 ping”可能根本没发出去,或者 pong 被中间设备吞掉了。
- 用 Wireshark 过滤
tcp.port == 8080,重点看 FIN/RST 包由哪端发出:客户端发 RST 多因 JS 主动调用close()后异常退出;服务端发 RST 多因进程崩溃、OOM 或负载过载 - 服务端开启
ws调试日志:const wss = new WebSocket.Server({ port: 8080, perMessageDeflate: false, log: console.log }) - 如果
onerror完全不触发,基本可锁定为网络中断或代理强制断连——因为onerror只在 WebSocket 协议层出错时触发,TCP 断开不在此列
真正棘手的 1006 往往藏在 Nginx 配置、Node.js HTTP 超时、移动端后台冻结策略这些“看不见”的地方,而不是你的 send 或 onmessage 逻辑里。

















