结论:在线人数统计不能只靠onOpen/onClose事件计数,必须配合心跳检测与主动断连识别;因页面刷新、网络抖动、浏览器崩溃等场景会导致onClose漏触发,连接假活堆积,人数虚高;需服务端定时PING、客户端及时PONG、超时强制下线,并在分布式环境下用Redis SET统一维护在线集合。

直接说结论:在线人数统计不能只靠 onOpen / onClose 事件计数,必须配合心跳检测 + 主动断连识别,否则页面刷新、网络抖动、浏览器崩溃都会导致人数虚高。
为什么 onOpen/onClose 不可靠?
这两个生命周期钩子看似天然对应“上线/下线”,但实际运行中大量场景会漏触发或误触发:
- 用户关闭标签页但未触发
onClose(Chrome 有时延迟数秒甚至不触发) - 用户切到其他应用、电脑休眠,WebSocket 连接处于“假活”状态,服务端无法感知
- 前端未调用
socket.close()就跳转页面,连接残留但客户端已失联 - 代理或 NAT 设备静默丢弃空闲连接,服务端 TCP 连接仍显示 ESTABLISHED
结果就是:服务端 onlineCount 只增不减,一小时后可能比真实用户多出 30%。
必须加心跳保活与超时剔除
真正可用的上下线逻辑,得靠“主动探测 + 被动兜底”双机制:
- 服务端每 15–30 秒向每个连接发送
PING消息(文本帧,内容可为"ping") - 客户端收到后必须立即回
PONG(如socket.send("pong")),不能只监听不响应 - 服务端维护每个连接的最后响应时间戳,若超过 45 秒无
PONG,强制session.close()并减人数 -
@OnMessage中需区分业务消息和心跳响应,避免把"pong"当作业务数据处理
Spring Boot 示例中,别只写 @OnMessage 处理业务;要额外加个 if ("pong".equals(message)) { updateLastActiveTime(session); return; }。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
前端如何避免重复注册或丢失更新?
Vue/React 里用 watchEffect 或 useEffect 同步人数时,常见错误是把 socket 实例和响应式变量耦合太紧:
- 不要在
watchEffect里调socket.onmessage—— 会导致每次响应式依赖变化都重新绑定,产生多个监听器 - 不要把
onlineCount挂在socket实例上(如socket.count),watchEffect无法追踪普通属性变化 - 正确做法:用
const onlineCount = ref(0)独立管理,所有消息回调里只做onlineCount.value = data.count - 连接重建时,先清空旧
socket的事件监听(socket.removeEventListener或用 AbortController),再赋新值
否则你会看到数字乱跳、DOM 更新两次、甚至内存泄漏 —— 因为旧 socket 的 onmessage 还在往已销毁的组件里塞数据。
分布式部署时在线数不准怎么办?
单机用 CopyOnWriteArraySet 或 ConcurrentHashMap 没问题,但上 Kubernetes 或多节点后,各实例的在线集合互不可见:
- 别用内存集合存总数,改用 Redis 的
SET结构存 session ID,用SCARD online:users获取实时数 - 每次
@OnOpen执行redisTemplate.opsForSet().add("online:users", sessionId) -
@OnClose和心跳超时处理都要同步删 Redis key:redisTemplate.opsForSet().remove("online:users", sessionId) - 注意 Redis 连接池配置,避免高并发下
set/scard成瓶颈;可考虑用 Lua 脚本合并读写
最易被忽略的一点:Redis key 的过期时间必须设为略长于心跳周期(比如 60 秒),否则刚连上就被自动清理。

















