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(协议声明) |
| ACK | ack 推进 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(本地构造)+ sendMessage | items 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 作为下一页游标 |
| 本地有、服务端没有 | 本地清理/服务端历史清理不一致 | 以服务端为准重新分页;本地缓存可重建 |
相关链接
- 消息可靠性与顺序:失败模式与对策
- 本地存储与离线时长:各端落库细节
- 消息进阶:扩展/回应/流式/合并
- IMLib 进阶:连接保活与重连:发送队列与重连配合
- 消息搜索与本地存储:本地检索与历史分页