
WebSocket消息丢了,光靠重连没用
重连只能解决连接断开,但断开前发出去的那几条消息,服务器可能根本没收到——前端以为发成功了,后端压根没处理。这时候必须靠应用层确认机制,不是靠网络层或 WebSocket 自带的 ping/pong。
-
onclose触发时,socket.bufferedAmount还大于 0,说明有消息卡在浏览器发送队列里,还没真正发出去 - 服务端主动断连(比如重启)时,不会等客户端消息发完,直接关连接,消息静默丢失
- 弱网下
send()调用返回true,不代表对方收到了,只代表塞进了浏览器的输出缓冲区
必须自己实现 ACK + 消息 ID
WebSocket 协议本身不提供应用级送达保证,你得自己加一层“已读回执”。核心就三样:唯一 ID、ACK 帧、超时重发。
- 每条业务消息带上客户端生成的
messageId(推荐crypto.randomUUID(),别用时间戳+随机数拼接,有碰撞风险) - 发送后启动定时器,比如
setTimeout(() => resend(msg), 5000),同时把msg存进一个待确认 Map:pendingMap.set(msgId, { msg, timer }) - 收到服务端返回的
{ type: 'ACK', messageId: 'xxx' }后,clearTimeout(pendingMap.get(msgId).timer)并删掉记录 - 重发时要检查是否已送达(比如用户已刷新页面),避免重复提交;服务端也要按
messageId去重,不能只靠序列号
心跳和 ACK 别混成一件事
心跳(ping/pong)保的是 TCP 连接活着,ACK 保的是某条业务消息被处理了——两者目的不同,不能互相替代,也不能共用同一套超时逻辑。
- 心跳间隔建议设为
25s,服务端ping,客户端必须响应pong;超过45s没响应就断连重试 - ACK 超时建议独立设置,比如
5s,比心跳短,否则你会误以为“连接还活着,消息肯定到了” - 别让 ACK 包走心跳通道:心跳帧是控制帧,不能携带业务字段;ACK 必须是普通文本消息,格式统一,例如
{"type":"ack","mid":"abc123"} - 服务端收到业务消息后,必须立刻发 ACK,不能等 DB 写完再发——否则 ACK 延迟导致前端反复重发
离线消息兜底不能只靠重发
用户切后台、关页面、杀进程,重发机制完全失效。ACK 只能解决“在线时的瞬时丢包”,离线状态得靠服务端缓存 + 状态同步。
立即学习“前端免费学习笔记(深入)”;
- 服务端收到消息但目标用户不在线,不能丢弃,要写入
redis或数据库,key 用userId:messageId,带过期时间(比如 7 天) - 用户重连成功后,客户端要主动发一个
{ type: 'fetch_offline', lastSeq: 12345 },服务端返回未读消息列表 - 注意:离线消息也要走 ACK 流程,否则“拉取成功”不等于“用户看到”,下次拉又会重复推
- 如果消息量大,别一次性全推,按分页或时间窗口(如最近 1 小时)控制,避免首屏卡顿
最容易被忽略的是:ACK 不是发出去就完了,它本身也是一条网络消息,也可能丢。所以服务端发 ACK 之前,得先落库标记“已触发 ACK”,失败则重试;客户端收 ACK 后,也要发个“ACK-RECEIVED”给服务端——这层嵌套确认,才是生产环境扛住弱网的关键。











