WebSocket消息可靠性需应用层实现过期校验、内存+持久化缓存、显式ACK及协同机制。服务端用可信时间判过期,客户端本地队列+指数退避重发,持久化存储保障重启不丢,ACK与业务成功严格对齐。

WebSocket 本身不提供消息过期、缓存或重发语义——这些必须由应用层自己实现。直接依赖 WebSocket 原生 API 发送消息,一旦连接断开或接收方崩溃,消息就彻底丢失。
Java 中为 WebSocket 消息添加过期时间(onMessage 阶段校验)
Java-WebSocket 或 Spring WebSocket 的 @OnMessage 方法收到消息后,需立即解析并检查时效性。不能靠客户端传“过期时间字段”就信任它,服务端必须用自己可信的时间源做判断。
- 消息体中应包含
timestamp(毫秒级时间戳)和可选的ttl(单位:秒),服务端计算timestamp + ttl * 1000 - 若过期,直接丢弃,不进入业务逻辑,也不返回任何 ACK —— 避免误导发送方
- 注意时钟漂移:若客户端和服务端时间偏差大,建议改用服务端生成的
messageId+ 单调递增序列号 + 服务端写入时间作为唯一判定依据 - Spring Boot 示例中,可在
@MessageMapping方法开头加if (isExpired(message)) throw new MessageDeliveryException("expired"),配合全局异常处理器统一响应
SocketRocket(iOS)内存缓存 + 自动清理策略
SRWebSocket 的 webSocket:didReceiveMessage: 回调是唯一入口,所有缓存逻辑必须在这里触发。缓存不是“先存再用”,而是“边存边判”,否则容易漏掉关键消息。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 使用
NSMutableArray存储NSDictionary或自定义MessageCacheItem,必须带receivedAt(NSDate)、priority(NSInteger)、isAcked(BOOL)三个字段 - 每次插入新消息后调用
pruneCache:先按isAcked == NO且receivedAt超过 60 秒的清理;再按总数 > 200 时 FIFO 截断 - 不要在主线程做耗时清理操作,把
pruneCache放进dispatch_queue_create("cache.prune", DISPATCH_QUEUE_SERIAL) - ⚠️ 容易踩坑:未对
message做深拷贝就直接存入数组,后续原对象被释放会导致野指针或 crash
可靠重发必须依赖持久化存储而非纯内存
纯内存缓存(如 ConcurrentHashMap 或 NSMutableArray)在进程重启后全部清零,无法支撑“至少一次”交付。真正可用的重发机制,必须把待确认消息落盘。
- 轻量场景用 Redis:以
message:{uuid}为 key,value 是 JSON 序列化的消息体 +sendTime+retryCount,设置EXPIRE为 24 小时 - 高一致性场景用数据库:建表
websocket_message_outbox,含id(UUID)、payload(TEXT)、status('pending'/'acked'/'failed')、next_retry_at(TIMESTAMP) - 重试调度器不能用
Timer或ScheduledExecutorService简单轮询,而应基于 “select top 100 where next_retry_at - ACK 必须由业务逻辑显式触发,不是收到消息就发 ACK —— 比如订单创建失败,就不能发 ACK,要等补偿成功后才确认
前端 WebSocket 客户端如何配合过期与重发
浏览器原生 WebSocket 对象没有任何内置缓存或重发能力。你写的“自动重连”代码,只管连上,不管连上之前丢了什么。
- 所有待发消息必须先入本地队列(如
localStorage或IndexedDB),再调用ws.send();发送成功后从队列删除 - 监听
onclose事件时,把队列里未标记sent的消息状态改为queued,等重连成功后批量重发 - 每条消息附带
__ts(客户端时间戳)和__ttl(毫秒),服务端据此判断是否还值得处理 - 别用
setTimeout实现“发不出就重试”——网络恢复瞬间大量setTimeout同时触发,会打爆服务端。应该用指数退避 + 随机抖动,比如Math.pow(2, retryCount) * 1000 + Math.random() * 500
消息过期不是加个字段就能生效,缓存不是存进去就完事,重发也不是循环调用 send。真正难的是三者协同:服务端能拒绝过期消息,客户端知道该重发哪些,持久化存储能扛住重启和并发,而 ACK 的语义必须和业务成功严格对齐。漏掉任一环,所谓的“可靠”就只是幻觉。

















