应手动筛选 websocket.open 状态的连接再广播,因 wss.clients 不自动剔除断开、关闭或未握手的 socket;需监听 close 事件清理、用心跳标记 isalive 并定时扫描,避免向失效连接发消息。

直接用 ws 库的 broadcast 逻辑就行,但必须自己维护连接池——ws 不内置“所有在线用户”这个概念,所谓“广播”只是你遍历当前存活的 socket 对象并调用 send()。
为什么不能直接 ws.broadcast()?
ws 库(v8.14+)确实有个 WebSocketServer.broadcast() 方法,但它只在服务端实例上可用,且要求你手动把每个新连接的 socket 显式存进 wss.clients 集合。而默认情况下,wss.clients 是一个 Set,它不自动过滤已断开、未握手或正在关闭的连接。
- 常见错误现象:
wss.clients.size显示有 5 个连接,但实际只有 3 个能收消息,另外 2 个触发socket.send()时抛出WebSocket is not open - 根本原因:客户端刷新页面、网络中断、未监听
close事件导致socket残留在wss.clients中 - 正确做法:改用
Map或WeakMap存储带状态的连接,或每次广播前先检查socket.readyState === WebSocket.OPEN
如何安全地向所有在线用户发送消息?
核心是「发之前先筛」。不要信任 wss.clients 的原始长度或迭代结果,尤其在高并发或频繁重连场景下。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 推荐写法:
for (const client of wss.clients) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: 'message', from: 'system', content: text })); } } - 避免写法:
wss.clients.forEach(client => client.send(...))—— 会 crash 掉已断开的连接 - 如果需要区分用户身份(比如昵称、ID),不要依赖
socket.url或 query 参数做判断,应在握手完成后的第一个消息里由客户端主动上报,并存在socket.userId = xxx上 - 注意:
WebSocket.CLOSING和WebSocket.CLOSED状态下调用send()会静默失败或抛错,必须排除
广播消息时容易忽略的边界情况
真实聊天室不是“发完就完”,很多问题出在连接生命周期管理上。
- 客户端刷新页面后,旧
socket还在wss.clients里,但已无法通信;必须监听socket.on('close', ...)并从你的用户映射表中清除对应项 - 心跳超时断连(如 ping/pong 超过 30s)不会自动触发
close事件,需配合socket.isAlive = true标记 + 定时扫描清理 - 服务端重启会导致所有连接丢失,但前端若开启自动重连(
ws.onclose → setTimeout(() => new WebSocket(...), 1000)),可能在重启窗口期产生重复登录,需加 token 或 session 去重逻辑 - 广播内容含敏感信息(如用户 IP、内部 ID)时,务必在服务端做脱敏,别指望前端 JS 过滤
真正麻烦的从来不是“怎么发”,而是“谁还活着”和“谁该收到”。连接状态永远比你想的更脆弱,别省那几行 if (client.readyState === WebSocket.OPEN)。










