onClose不可靠,需分层清理:监听底层断开事件、心跳超时兜底、HTTP会话辅助;onClose内仅做轻量收尾,状态更新须幂等且基于多连接+心跳判定。

在 onClose 回调里清理用户数据是可行的,但不能只依赖它——它不保证总被触发,也不代表“用户已离线”的最终状态。
onClose 被跳过或延迟的常见场景
WebSocket 的 onClose(或类似命名的回调,如 Java-WebSocket 的 onClose、OkHttp 的 onClosed、ASP.NET Core 的 CloseAsync 后续逻辑)本质是协议层握手完成后的通知。但它可能根本不会执行:
- 客户端直接断网、杀进程、关标签页 → TCP 连接静默中断,服务端收不到关闭帧,
onClose永不触发 - 服务端未正确响应关闭帧(如阻塞未处理),导致握手卡住,超时后强制断连,部分库跳过回调
- 使用了
close(1006)或未发送关闭帧就调用底层socket.close()→ 协议违规,对端无法进入标准关闭流程
真正可靠的用户数据清理时机
不能把所有希望押在 onClose 上。真实系统中,清理必须分层、多点触发:
-
连接断开瞬间(首选):监听 WebSocket 的底层断开事件(如 Netty 的
channelInactive、Spring WebFlux 的connection.closeFlux 终止),比onClose更早、更可靠 - 心跳超时兜底:每 15–30 秒发一次 ping,客户端需回 pong;若连续 2–3 次无响应,主动标记为离线并清理数据库会话、缓存用户状态等
-
HTTP 会话过期辅助:虽不能实时,但可作为二级清理手段(例如清除
HttpSession关联的 WebSocket session ID 映射)
示例(Java Spring Boot + WebSocket):
webSocketSession.close(); // 触发 onClose
// ✅ 但同时要注册:
session.getAttributes().put("lastHeartbeat", System.currentTimeMillis());
// 后台定时任务扫描 lastHeartbeat > 60s 的 session 并 cleanup()
onClose 里该做且只能做的几件事
如果 onClose 真的执行了,说明握手成功完成,此时适合做轻量、快速、无副作用的收尾:
- 从内存 Map 中移除该
WebSocketSession引用(避免内存泄漏) - 取消该连接绑定的定时任务(如未完成的消息重发 timer)
- 记录日志:
"User {} closed connection: code={}, reason={}"—— 仅用于审计,不用于状态变更 - ⚠️ 不要在这里同步调用数据库更新在线状态,更不要发起远程 RPC —— 它可能阻塞关闭流程,甚至导致握手失败
为什么数据库状态更新不能只靠 onClose
因为用户状态是业务概念,不是协议概念。一个用户可能有多个标签页(多个 WebSocket 连接),onClose 只反映单个连接终结;也可能页面没关但网络已断,onClose 压根不触发。所以真正的“用户离线”判定必须基于:最后一次心跳时间 + 所有连接是否全部断开 + 可选的客户端显式登出上报。
最容易被忽略的一点:清理动作本身要有幂等性。同一个用户可能因网络抖动反复触发断开/重连,onClose 和心跳超时可能并发执行,数据库更新语句必须支持重复执行不报错(比如用 UPDATE ... SET online = false WHERE user_id = ? AND online = true)。










