WebSocket断开后消息补偿本质是状态驱动的可靠投递,需结合心跳超时标记在线状态、按消息归属与投递状态持久化存储、重连时依据last_received_id差集补发,并配合客户端幂等消费。
websocket 断开后消息补偿,本质不是“补发”而是“状态驱动的可靠投递”。直接往断开的 session 调用 session.getbasicremote().sendtext() 必然抛 illegalstateexception: the session is closed —— 这是绝大多数人卡住的第一步。
为什么 ConcurrentHashMap<string session></string> 不能直接用来做离线补偿
很多人一上来就用 ConcurrentHashMap 缓存 userId → Session,以为“查不到就是离线,存库就行”。但问题在于:
- 用户断网但服务端还没触发
@OnClose(比如心跳超时前网络已中断),Session对象仍存在,但调用sendText()会静默失败或抛异常 -
Session是非线程安全对象,跨线程复用(如异步消息线程去调它)可能引发IOException或连接重置 - 多实例部署下,
ConcurrentHashMap只在单机有效,其他节点根本看不到这个Session
所以缓存 Session 只能用于“在线快速推送”,绝不能作为判断“是否可投递”的唯一依据。真正可靠的在线状态,必须结合心跳上报 + 服务端超时标记(比如 Redis 中 user:online:{userId} TTL 30s)。
WebSocketSession 关闭后,怎么知道该补哪些消息
关键不是“记住消息内容”,而是“记住消息归属 + 投递状态”。推荐用 MySQL 或 Redis 实现轻量级消息队列:
- 每条业务消息写入
message_queue表,字段至少包含:id、target_user_id、payload(JSON)、status('pending'/'sent'/'failed')、created_at - 用户上线时,查
SELECT * FROM message_queue WHERE target_user_id = ? AND status = 'pending',批量推送并原子更新状态为'sent' - 不要等客户端确认后再改状态——先推,再由客户端回执触发二次状态更新(如
'delivered'),避免补推逻辑被阻塞
如果选 Redis,用 Sorted Set 按时间戳排序,ZRANGEBYSCORE message_queue:{userId} -inf +inf 拉取待投消息,比 List 更易支持按时间范围过滤和去重。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
重连后消息不重复、不丢失的边界条件怎么控
重连本身不等于“状态续接”。客户端重连成功后,必须主动携带上次连接的最后已收消息 ID(比如 last_received_id: 12345),服务端据此做差集补发:
- 服务端收到重连请求,先查该用户
message_queue中id > 12345 AND status = 'pending'的消息 - 推送完立即执行
UPDATE message_queue SET status = 'sent' WHERE id IN (...),避免网络抖动导致多次重连反复补发 - 客户端必须在页面/APP 初始化阶段,从本地存储(
localStorage/SharedPreferences)读取上一次的last_received_id,而不是靠服务端“猜”
漏掉这个 ID 传递环节,所有补发逻辑都会退化成“全量拉取”,在高并发重连场景下极易压垮数据库或 Redis。
最常被忽略的一点:消息补发不是“推完就完”,要配合客户端的幂等消费。服务端补发时带 message_id,客户端收到后先查本地是否已处理过该 ID —— 这层校验比服务端去重更轻量、更可靠。










