WebSocket本身不支持离线消息,连接断开后消息丢失;需前后端协同实现:服务端查会话状态、离线消息写库、重连后查询推送,前端握手确认后拉取并去重渲染。

WebSocket 本身不支持离线消息——连接断开,服务端就无法投递。所谓“用户上线后补发”,必须靠前后端协同设计缓存、状态跟踪和重投逻辑,不是开个 WebSocket 就自动有的功能。
WebSocket 连接断开时,消息到底去哪了?
原生 WebSocket 协议没有队列、没有确认、没有持久化。一旦 session.close() 或网络中断,ws.send() 调用直接失败,错误被静默吞掉(除非你显式监听 onerror),消息就丢了。
常见错误现象:
- 用户切后台/锁屏几秒后收不到新消息
- 页面刷新瞬间发的消息“消失”
- 服务端调用
sessions.get(userId).sendMessage(...)报IllegalStateException: The session is closed
根本原因:服务端只维护内存级会话映射(如 ConcurrentHashMap<string websocketsession></string>),没做任何离线兜底。
SpringBoot 后端如何标记并暂存离线消息
关键不在“推”,而在“判 + 存 + 查”。必须拆成三步走:
- 发送前先查
WebSocketSession是否还isOpen(),否则跳过直推,进入离线流程 - 离线消息写入数据库表(如
msg_offline),字段至少含:user_id、content、status(0=未推/1=已推/2=失败)、created_at - 用户重连成功时(在
afterConnectionEstablished回调里),立即查该user_id的status = 0消息,批量推送并更新status = 1
注意点:
- 不要用
@SendToUser自动路由——它只对当前在线会话有效,离线时直接丢弃 - 避免在
WebSocketHandler中直接操作数据库,应注入MessageService解耦 - 查离线消息建议加
FOR UPDATE或用乐观锁,防止并发重连时重复推送
前端重连后怎么触发补发?别依赖 onopen
WebSocket.onopen 只表示连接建立,不代表业务层已准备好。真正可靠的做法是:服务端在连接建立后主动下发一个握手响应,前端收到后再拉取离线消息。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
示例流程:
- 前端连接成功后,发一条
{ type: "handshake", client_id: "xxx" }给服务端 - 服务端收到后,查该用户的离线消息,封装为
{ type: "offline_batch", messages: [...] }推回 - 前端收到
offline_batch后,逐条渲染,并清空本地未读计数
容易踩的坑:
- 直接在
onopen里调用fetch("/api/offline")——此时服务端可能还没完成会话注册,查不到用户关联关系 - 前端用
localStorage缓存未读数但不同步服务端状态,导致数字不准 - 补发消息没带时间戳或 ID,前端无法判断是否重复渲染
消息幂等与重复投递怎么防?
补发机制天然带来重复风险:网络抖动导致重连多次、服务端重试逻辑触发、前端手动刷新等。防重必须前后端配合。
服务端层面:
- 每条离线消息生成唯一
msg_id(如 UUID 或雪花 ID) - 推送时把
msg_id带过去,客户端 ACK 后,服务端才更新数据库status - 数据库加唯一索引:
UNIQUE KEY (user_id, msg_id),避免同 ID 消息重复插入
前端层面:
- 收到消息先检查
msg_id是否已在本地Set中存在,有则跳过 - 不要用“收到即渲染”,改为“收到 → 存入队列 → 按时间戳排序 → 去重 → 渲染”
- 页面卸载前(
beforeunload)把未 ACK 的msg_id存入localStorage,恢复时校验
最易被忽略的一点:补发逻辑只解决“用户离线期间别人发给他的消息”,不解决“他自己发出去但服务端没收到”的问题——那属于发送端可靠性,得另做客户端待发队列 + 服务端 ACK 闭环。

















