消息可靠性与顺序

本项目承诺的是至少一次投递 + 会话内有序 + 客户端去重,不是“恰好一次”。理解这一点,才能正确实现发送重试、离线补收、已读游标与多端一致性。

契约保证​

维度保证客户端职责
投递at-least-once(客户端按 messageId 去重)按 messageId 去重;重复消息只渲染/入库一次
发送幂等同用户 clientMsgId 10 分钟内幂等重试复用同一 clientMsgId
顺序同一会话内 seq 单调递增;跨会话无全局序同一会话按 seq 排序,不按到达时间
补收hello 后自动补发离线消息,随后 sync_done以 sync_done 作为同步完成信号
游标推进仅当 ack 的 messageId 属于当前用户可达消息时推进游标;乱序 ack 取已确认最大连续 seq按服务端可达消息推进,处理乱序 ack
故障切换服务端重启后内存会话游标丢失时,以持久化消息与客户端 seq 补拉恢复(v1.0.0 演示实现:内存态+可选 PostgreSQL)以持久化消息 + seq 补拉恢复
离线队列每会话 500 条超出部分需历史分页拉取
“恰好一次”不存在

网络超时后你无法知道消息是否已入库:唯一安全做法是用同一 clientMsgId 重试,由服务端幂等去重;客户端再按 messageId 去重收端。任何“超时就换新 ID 重发”的实现都会产生重复消息。

正确实现的骨架​

JavaScript
import { createClient, ConversationType, Events } from '@ni-paas/im-web';

const client = createClient({ appkey: 'YOUR_APP_KEY', serverUrl: 'http://127.0.0.1:8080' });
const seen = new Set();          // 收端去重(生产按 messageId 持久化)
const pending = new Map();       // 发端待确认:clientMsgId -> {type,targetId,content}

async function sendOnce(type, targetId, content) {
  const clientMsgId = crypto.randomUUID(); // 同一业务消息的重试必须复用
  pending.set(clientMsgId, { type, targetId, content });
  const res = await client.sendMessage(type, targetId, content, { clientMsgId });
  if (res.code === 0) pending.delete(clientMsgId);
  return res;
}

// 断线恢复后:对未确认消息用原 clientMsgId 重发(服务端幂等)
async function resendPending() {
  for (const [clientMsgId, item] of pending) {
    const res = await client.sendMessage(item.type, item.targetId, item.content, { clientMsgId });
    if (res.code === 0 || ![3001, 3002, 3003, 1008].includes(res.code)) pending.delete(clientMsgId);
  }
}

client.addEventListener(Events.MESSAGES, ({ messages }) => {
  const fresh = messages.filter((m) => !seen.has(m.messageId));
  fresh.forEach((m) => seen.add(m.messageId));
  renderSortedBySeq(fresh); // 同一会话按 seq 排序
});

失败模式与对策​

失败模式现象对策
发送超时调用方不知道是否成功复用 clientMsgId 重试;收端去重
服务端重启内存游标丢失按持久化消息与 seq 补拉;以 sync_done 对齐
离线队列溢出超过 500 条的部分不在补发里重连后按历史分页补齐;重要消息用业务归档
重复投递收端收到两条相同消息按 messageId 去重(不是 clientMsgId)
乱序到达后发消息先到按 seq 重排,不要用本地时间
半开连接TCP 还在但无法收发依赖 ping/pong 与空闲超时;SDK 判定后重连

相关页面​