IMLib 进阶:消息收发与本地存储

消息链路的正确性由四件事决定:至少一次投递、客户端去重、会话内顺序、本地可见性。本页把发送状态机、高级消息能力(扩展/回应/流式/合并)、离线补收与各端本地存储放在一起,作为接入后的第二层阅读材料。

发送状态机​

状态含义进入条件下一步
pending本地已渲染,等待服务端确认调用发送接口后立即成功 → sent;失败 → failed(可重发)
sent服务端已接收并分配 messageId/seq收到发送响应或 ack按 seq 插入有序列表
failed发送失败(可重试/不可重试)错误码可重试且超过重试次数,或不可重试保留 clientMsgId,重发用同一幂等键
recalled已撤回收到 MESSAGE_RECALLED渲染撤回占位,不再参与搜索

投递语义是 at-least-once(客户端按 messageId 去重):客户端必须按 messageId 去重。发送幂等:同用户 clientMsgId 10 分钟内幂等;服务端按 clientMsgId 去重,客户端按 messageId 去重。

顺序与分页​

项规则
会话内顺序同一会话内 seq 单调递增;跨会话无全局序
历史分页getHistoryMessages:beforeSeq 向前翻页,默认 20、上限 100,返回 hasMore
离线补收重连后 hello 给出 syncState.deliveredSeq,补发完成后下发 sync_done;单会话补发上限 500(协议声明)
ACKack 推进 deliveredSeq;乱序 ack 只推进连续已确认的最大 seq
消息体积单条序列化上限 5120 字节;文本 5000 字符

高级消息能力​

能力关键接口限制文档
消息扩展(KV)updateMessageExpansion / getMessageExpansion / removeMessageExpansion仅单聊/群聊/超级群且发送时开启可扩展;单次 ≤20 对、单条 ≤300 对、value ≤4096消息进阶
表情回应addMessageReaction / removeMessageReaction / batchGetMessageReactionSummaries单条最多 50 种回应、每种最多 100 人;reactionId 1-16 字符;重复添加幂等消息进阶
流式消息startStreamMessage / appendStreamMessage / completeStreamMessage / getStreamMessage支持单聊/群聊/系统通知,聊天室不支持(1001);仅发送者可追加;seq 重复覆盖该段消息进阶
合并转发combineMessages(本地构造)+ sendMessageitems 1..100、title ≤64 字、条目合计 ≤16384 字节,序列化后仍受 5120 字节限制(超限 1007)消息进阶
流式事件STREAM_MESSAGE_CHANGED(stream_changed)增量与结束后各推一次;complete=true 时落库为普通文本消息连接监听

本地存储与可见性​

端消息本地库重启后说明
Android系统 SQLite(按 owner 隔离)保留公开 API 无查询/清空;不要直接读库
Electron本地 SQLite(localDb 26 个方法)保留支持本地搜索、按时间清理、会话列表读取
Web内存丢失刷新后依赖 REST 重新拉取;IMKit.store 只存非消息数据
Flutter实例内存丢失实例重建后重新拉取
Native无丢失通过轮询帧获取事件

三端发送与补收代码​

import { Events } from '@ni-paas/im-web';

// 1) 发送:clientMsgId 复用业务幂等键,失败重发不会重复。
async function send(text, orderId) {
  const res = await client.sendTextMessage(1, "user2", text, { clientMsgId: orderId });
  if (res.code !== 0) queueForRetry(orderId, text);
  return res;
}

// 2) 收消息:先按 messageId 去重,再按 seq 插入。
const seen = new Set();
client.addEventListener(Events.MESSAGES, ({ messages }) => {
  for (const message of messages) {
    if (seen.has(message.messageId)) continue;
    seen.add(message.messageId);
    insertBySeq(message);
  }
});

// 3) 补收完成后再对齐一次历史,覆盖极端情况(本地缺失/清理过)。
await client.connect(token);
await syncHistoryForVisibleConversations();
client.addEventListener(IMClientEvent.MESSAGES, payload -> {
    for (Message message : payload.getMessages()) {
        if (seen.add(message.getMessageId())) insertBySeq(message);
    }
});

client.sendTextMessage(ConversationType.PRIVATE, "user2", "你好", orderId,
    new SendMessageCallback() {
        @Override public void onSuccess(Message message) { markSent(message); }
        @Override public void onError(IMError error) { queueForRetry(orderId, error); }
    });
final seen = <String>{};
client.addEventListener(RongIMEvents.MESSAGES, (payload) {
  for (final message in payload.messages) {
    if (seen.add(message.messageId)) insertBySeq(message);
  }
});

final res = await client.sendTextMessage(1, "user2", "你好", clientMsgId: orderId);
if (res.code != 0) queueForRetry(orderId);

常见问题与对策​

现象原因对策
收到重复消息at-least-once 重投按 messageId 去重(持久化去重集合)
消息顺序错乱并发插入 / 未按 seq 排序同一会话按 seq 有序插入;离线补发后再对齐
发送一直 pending未连接或长连接被挂起等待 CONNECTED;检查 3001/3002/3003
重发产生两条消息重试换了 clientMsgId同一业务消息复用同一幂等键
历史翻页重复/漏条使用了错误的 beforeSeq用返回列表里最小 seq 作为下一页游标
本地有、服务端没有本地清理/服务端历史清理不一致以服务端为准重新分页;本地缓存可重建

相关链接​