弱网与重连

弱网下最难的不是“重连”,而是重连后状态一致:消息不丢、不重复、顺序可解释、游标能推进。本页给出协议层的确定性参数、客户端必须实现的状态机、可复现的弱网测试矩阵与常见事故。

协议参数(来自冻结契约)​

参数值含义
客户端 ping 间隔30000ms需要主动保活;服务端依赖活动判定
服务端空闲超时90000ms超过即断开连接
要求 pong是未回 pong 视为链路异常
连续未 pong 重连阈值60000ms达到后客户端主动重连
重连策略exponential-backoff-with-jitter指数退避 + 抖动
退避区间1000ms → 30000ms避免同时重连造成风暴
最大尝试次数8超过后进入挂起,等网络/前台事件恢复
离线补发hello 后自动补发重连成功即开始补收
设备级补收(rev25)WS query 可选 deviceId(SDK 自动生成/持久化)按设备游标补发;A 设备 ack 不影响 B;无 deviceId 走 legacy 用户级游标
补收完成信号sync_done客户端应以 sync_done 作为“历史已对齐”的时刻
投递语义at-least-once(客户端按 messageId 去重)客户端必须按 messageId 去重
顺序同一会话内 seq 单调递增;跨会话无全局序同一会话内 seq 单调;跨会话无全局序
发送幂等同用户 clientMsgId 10 分钟内幂等重试必须复用 clientMsgId
没有 sync_done 就宣布“同步完成”是错的

重连成功(CONNECTED)只代表长连接建立,离线消息还在补发。UI 应显示“同步中”,直到收到 sync_done 或重放结束;否则首屏可能缺消息。

客户端状态机​

状态进入条件应该做什么
CONNECTING调用 connect / 自动重连显示连接中;暂停非必要请求;不要重复 connect
CONNECTED握手成功、hello 到达恢复订阅;开始接收离线补发;发送待发队列
SUSPEND网络切换/系统挂起/心跳异常保留待发队列;按退避重连;不要清空本地消息
DISCONNECT主动断开或超过重试上限停止自动重连;等待用户操作或网络恢复事件
KICKED服务端踢下线(封禁/多端策略)停止重连、清理登录态、提示原因码
JavaScript
import { createClient, Events } from '@ni-paas-im-web';

const client = createClient({ appkey: 'YOUR_APP_KEY', serverUrl: 'http://127.0.0.1:8080' });
let syncing = true;

client.addEventListener(Events.CONNECTING, () => setStatus('连接中'));
client.addEventListener(Events.CONNECTED, () => { setStatus('同步中'); syncing = true; });
client.addEventListener(Events.SUSPEND, () => setStatus('网络不稳定'));
client.addEventListener(Events.DISCONNECT, ({ reason }) => setStatus(`已断开:${reason}`));
client.addEventListener(Events.KICKED, ({ code, reason }) => {
  stopReconnect();
  logout(`被踢下线:${code} ${reason}`);
});

// 离线补发通过 MESSAGES 事件到达;收到 sync_done 后再标记同步完成
client.addEventListener(Events.MESSAGES, ({ messages, syncDone }) => {
  for (const message of messages) {
    if (!seen.has(message.messageId)) { // 按 messageId 去重(at-least-once)
      seen.add(message.messageId);
      store(message);
    }
  }
  if (syncDone || messages.some((m) => m.syncDone)) { syncing = false; setStatus('已同步'); }
});

发送侧:重试与去重​

问题正确做法错误做法
发送超时用同一 clientMsgId 重发(10 分钟幂等)生成新 clientMsgId 重发 → 重复消息
断线期间点发送进入本地待发队列,连接恢复后按序发送直接丢弃或无限弹错
收到重复消息按 messageId 去重后再入库/渲染按 clientMsgId 去重(服务端可能没有该字段回传)
顺序错乱同一会话按 seq 排序渲染按本地到达时间排序
重连风暴使用 SDK 内置退避,不要自己并发重连页面每次失败都 new client

弱网测试矩阵​

场景注入方式通过标准
高延迟单向 200ms消息最终送达;p95 时延可解释;UI 无假死
丢包5% 丢包 + TCP 重传0 丢失 / 0 重复(客户端去重后)
硬切断直接断 socket重连 + 离线补收;sync_done 后消息完整
网络切换Wi-Fi ↔ 移动网络SUSPEND → CONNECTING → CONNECTED,自动补 join 聊天室
弱网 + 大量离线断线期间写入 500 条补收不中断 UI;分页/批量入库
长时间抖动30 分钟随机丢包/延迟内存无持续增长;无 false-dead(假死连接)

常见事故​

现象根因修复
重连后聊天室收不到消息只恢复了 WS,没有补 join 或 join 超时 3002确认 SDK 自动补 join;失败时提示并重试
首屏少消息CONNECTED 就渲染完成、没等 sync_done用 syncing 状态门控
重复消息重试换了 clientMsgId / 没按 messageId 去重去重表 + 固定 clientMsgId
在线状态长期错误在线状态是快照查询,不订阅按需查询或订阅存在回调;不要缓存过久
服务端判死连接客户端未按 30s ping / 90s 空闲使用 SDK 心跳,不要自己发业务 ping 代替

相关页面​