一、简介
IM消息收发流程是即时通讯系统的核心环节,直接影响用户体验和系统可靠性。一个优秀的消息收发方案需要在消息可靠性 、发送延迟 、实现复杂度等多个维度之间取得平衡。
本方案文档从企业级IM的实际需求出发,详细分析了业界主流的三种消息收发流程方案:
- 基于ACK确认的可靠传输方案 - 通过ACK确认机制确保消息送达,适合对消息可靠性要求极高的场景,如企业级IM和金融类应用
- 消息序列号与同步方案 - 利用序列号实现消息去重和离线同步,支持大规模用户场景,天然处理消息乱序问题
- 混合队列+心跳确认方案 - 结合乐观更新和队列重试机制,在保证用户体验的同时,降低网络开销和服务器压力
本文档将深入分析每种方案的流程设计 、技术实现 、优缺点 以及适用场景,帮助项目团队根据实际需求选择最合适的消息收发流程方案。
二、业界生产企业级方案
2.1 方案一: 基于ACK确认的可靠传输方案
2.1.1 流程图
后端数据库 客户端数据库 服务器 消息队列 会话管理器 UI层 用户 后端数据库 客户端数据库 服务器 消息队列 会话管理器 UI层 用户 发送消息流程 超时重试流程 alt [重试次数 < 最大重试次数] [重试次数 >= 最大重试次数] 发送消息 生成消息ID 更新消息状态为"发送中" 立即显示消息 更新会话(临时) 加入待确认队列 发送消息到服务器 持久化消息 更新会话最后消息 返回ACK确认 清除超时定时器 更新消息状态为"已发送" 更新会话(确认) 持久化会话到客户端数据库 更新UI显示 从队列中移除 超时未收到ACK 重新发送消息 返回ACK 更新消息状态为"失败" 从队列中移除
2.1.2 流程描述
[前端主进程] 发送流程:
- 用户在UI层(渲染进程)发送消息,通过IPC通知主进程
- 主进程生成唯一的消息ID
- 渲染进程将消息状态设置为"发送中",并在聊天界面显示消息(乐观更新)
- 渲染进程临时更新会话信息(更新最后消息预览、活跃时间)
- 主进程将消息加入待确认队列,记录重试次数、超时定时器等信息
- 主进程通过WebSocket将消息发送到服务器
- 主进程等待服务器返回ACK确认
[服务端+前端主进程] 确认流程:
服务端部分:
- 服务器接收并处理消息,持久化到后端数据库
- 服务器在后端数据库中更新会话的最后消息信息
- 服务器向主进程返回ACK确认,包含消息ID和成功状态
前端主进程部分:
- 主进程收到ACK后,清除消息对应的超时定时器
- 更新消息状态为"已发送",通过IPC通知渲染进程
- 从待确认队列中移除该消息
- 主进程的会话管理器正式更新会话信息(确认会话状态)
- 主进程的会话管理器将会话信息持久化到客户端数据库
- 渲染进程UI层更新会话列表显示
[前端主进程] 重试流程:
- 如果消息在超时时间内未收到ACK,主进程触发超时处理
- 检查当前重试次数是否达到上限(默认3次)
- 如果未达到上限,递增重试次数,重新发送消息
- 如果达到上限,标记消息为"失败",从队列移除,通过IPC通知渲染进程
- 注意: 即使消息发送失败,临时会话信息仍保留(不回滚)
2.1.3 优缺点分析
优点:
- 消息可靠性高,有明确的发送确认机制
- 实现简单直观,易于调试和维护
- 适合对消息送达率要求极高的场景(如金融、企业IM)
- 重试机制保证在网络抖动情况下消息最终送达
- 每条消息都有明确的状态追踪
缺点:
- 每条消息都需要ACK确认,增加网络开销
- 延迟较高,需要等待ACK确认才能完成流程
- 重试机制可能导致消息乱序(尤其在快速发送多条消息时)
- 服务端需要维护消息状态,增加服务器压力
- 超时设置需要权衡: 太短增加重试,太长影响用户体验
2.1.4 适用场景
- 企业级IM应用
- 金融类应用(对消息可靠性要求极高)
- 用户量中等,对消息实时性要求不高的场景
- 需要明确消息送达状态的应用
2.1.5 代码实现
点击展开/收起 [前端主进程] 消息ACK确认机制
typescript
/**
* [伪代码示例]
*
* [前端主进程] 消息ACK确认机制
* 每条消息发送后等待服务器ACK确认,超时自动重试
*
* 说明: 该类位于前端主进程中,负责管理待确认消息队列、发送消息、等待ACK等
* 通过IPC与渲染进程通信,渲染进程负责UI展示
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 待确认消息类型定义
interface PendingMessage {
message: Message;
retryCount: number;
timeoutTimer: NodeJS.Timeout;
resolve: (value?: unknown) => void;
reject: (reason?: any) => void;
}
// 配置类型定义
interface AckMessageManagerConfig {
ackTimeout: number; // ACK超时时间(毫秒)
maxRetryCount: number; // 最大重试次数
}
class AckMessageManager {
// 待确认的消息Map: messageId -> PendingMessage
private pendingMessages: Map<string, PendingMessage> = new Map();
// 配置信息
private config: AckMessageManagerConfig;
constructor(config: AckMessageManagerConfig) {
this.config = config;
}
// 发送消息并等待ACK
async sendMessageWithAck(message: Message): Promise<void> {
const messageId = message.id;
// 设置消息状态为发送中
message.status = 'sending';
// 创建Promise等待ACK
const ackPromise = new Promise((resolve, reject) => {
// 设置超时定时器
const timeoutTimer = setTimeout(() => {
this.handleMessageTimeout(messageId);
reject(new Error('消息发送超时'));
}, this.config.ackTimeout);
// 存储待确认消息
this.pendingMessages.set(messageId, {
message,
retryCount: 0,
timeoutTimer,
resolve,
reject
});
// 发送消息到服务器(实际实现需要处理网络连接)
this.sendToServer(message);
});
return ackPromise;
}
// 处理服务器返回的ACK
handleAck(messageId: string, success: boolean) {
const pending = this.pendingMessages.get(messageId);
if (!pending) return;
// 清除超时定时器
clearTimeout(pending.timeoutTimer);
if (success) {
// 更新消息状态为已发送
pending.message.status = 'sent';
pending.resolve();
} else {
// 发送失败,触发重试
pending.reject(new Error('服务器拒绝消息'));
}
// 从待确认列表移除
this.pendingMessages.delete(messageId);
}
// 消息超时处理
private handleMessageTimeout(messageId: string) {
const pending = this.pendingMessages.get(messageId);
if (!pending) return;
if (pending.retryCount < this.config.maxRetryCount) {
// 未超过最大重试次数,重新发送
pending.retryCount++;
this.sendToServer(pending.message);
console.log(`消息重试 ${pending.retryCount}/${this.config.maxRetryCount}`);
} else {
// 超过最大重试次数,标记为失败
pending.message.status = 'failed';
pending.reject(new Error('消息发送失败,超过最大重试次数'));
this.pendingMessages.delete(messageId);
}
}
// 发送消息到服务器的方法(需要根据实际网络层实现)
private sendToServer(message: Message): void {
// 实际实现需要处理WebSocket发送、HTTP请求等
// 这里只是示例占位符
console.log('发送消息到服务器:', message);
}
/**
* 通过IPC通知渲染进程更新消息状态
*/
private notifyRenderProcess(event: string, data: any): void {
// 实际实现需要通过 electron 的 webContents.send 发送消息到渲染进程
console.log(`通知渲染进程 [${event}]:`, data);
}
}
点击展开/收起 [后端服务器] WebSocket服务器ACK处理逻辑 & 服务器端会话管理
typescript
/**
* [伪代码示例]
*
* [后端服务器] WebSocket服务器ACK处理逻辑
*
* 说明: 该类位于后端服务器,负责接收客户端消息、处理消息业务逻辑、发送ACK确认、推送消息给目标用户
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 消息类型定义(假设已定义)
// interface Message {
// id: string;
// content: string;
// senderId: string;
// conversationId: string;
// timestamp: number;
// status: 'sending' | 'sent' | 'failed';
// }
class WebSocketServerHandler {
// 收到客户端消息后的处理流程
async handleClientMessage(client: WebSocket, message: Message) {
try {
// 1. 解析并验证消息
const validatedMessage = await this.validateMessage(message);
// 2. 处理消息业务逻辑
await this.processMessage(validatedMessage);
// 3. 发送ACK确认
this.sendAck(client, {
messageId: validatedMessage.id,
success: true,
timestamp: Date.now()
});
// 4. 推送给目标用户
await this.pushToRecipients(validatedMessage);
} catch (error) {
// 发送失败ACK
this.sendAck(client, {
messageId: message.id,
success: false,
error: error.message
});
}
}
// 验证消息的方法(实际实现需要完整验证逻辑)
private async validateMessage(message: Message): Promise<Message> {
// 实际实现需要验证消息格式、权限等
console.log('验证消息:', message);
return message;
}
// 处理消息业务逻辑
private async processMessage(message: Message): Promise<void> {
// 实际实现可能包括:保存消息、更新会话、推送通知等
console.log('处理消息业务逻辑:', message);
}
// 发送ACK确认
private sendAck(client: WebSocket, ackData: {
messageId: string;
success: boolean;
timestamp: number;
error?: string;
}): void {
// 实际实现需要通过WebSocket发送ACK
console.log('发送ACK确认:', ackData);
}
// 推送给目标用户
private async pushToRecipients(message: Message): Promise<void> {
// 实际实现需要查找目标用户的连接并推送消息
console.log('推送消息给目标用户:', message);
}
}
typescript
/**
* [伪代码示例]
*
* [后端服务器] 服务器端会话管理 - ACK确认方案
*
* 说明: 该类位于后端服务器,负责处理消息时更新会话、持久化会话到数据库
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 会话类型定义(假设已定义)
// interface Session {
// id: string;
// participants: string[];
// lastMessageId: string;
// lastMessageContent: string;
// lastMessageTime: number;
// createdAt: Date;
// updatedAt: Date;
// }
class ServerSessionManager {
/**
* 处理消息时更新会话
*/
async updateSessionOnMessage(message: Message): Promise<void> {
// 1. 检查会话是否存在
let session = await this.getSessionById(message.conversationId);
if (!session) {
// 2. 会话不存在,创建会话
session = await this.createSession({
id: message.conversationId,
participants: message.participants,
createdAt: new Date()
});
}
// 3. 更新会话的最后消息
session.lastMessageId = message.id;
session.lastMessageContent = message.content;
session.lastMessageTime = message.timestamp;
session.updatedAt = new Date();
// 4. 持久化到数据库
await this.saveSession(session);
}
// 根据会话ID获取会话(实际实现需要数据库查询)
private async getSessionById(sessionId: string): Promise<Session | null> {
// 实际实现需要从数据库查询
console.log('查询会话:', sessionId);
return null; // 假设不存在
}
// 创建新会话
private async createSession(sessionData: {
id: string;
participants: string[];
createdAt: Date;
}): Promise<Session> {
// 实际实现需要创建会话并保存到数据库
console.log('创建会话:', sessionData);
return {
...sessionData,
lastMessageId: '',
lastMessageContent: '',
lastMessageTime: 0,
updatedAt: new Date()
};
}
// 保存会话到数据库
private async saveSession(session: Session): Promise<void> {
// 实际实现需要持久化到数据库
console.log('保存会话到数据库:', session);
}
}
2.2 方案二: 消息序列号与同步方案
2.2.1 流程图
后端数据库 客户端数据库 服务器 去重缓存 本地序列号 会话管理器 UI层 客户端 用户 后端数据库 客户端数据库 服务器 去重缓存 本地序列号 会话管理器 UI层 客户端 用户 发送消息流程 接收消息流程 alt [seqId <= serverSeqId] [seqId > serverSeqId] alt [消息已存在] [消息不存在] 离线同步流程 发送消息 生成本地序列号 (localSeq++) 添加序列号到消息 立即显示消息 (乐观更新) 更新消息状态为"已发送" 立即更新会话 (乐观更新) 更新sessionMsgSeqId 持久化会话到客户端数据库 发送消息 (不等待ACK) 持久化消息 分配全局序列号 (serverSeqId) 更新会话的sessionMsgSeqId 更新用户序列号 返回服务器序列号 (可选) 推送消息 (带seqId) 检查去重缓存 忽略重复消息 添加到去重缓存 检查序列号合法性 忽略过时消息 更新服务器序列号 更新会话 更新sessionMsgSeqId 计算未读数 持久化会话到客户端数据库 处理并显示消息 同步确认 (可选) 请求离线消息 (since: lastSeqId) 查询seqId > lastSeqId的消息 返回离线消息列表 批量处理消息 批量更新会话 批量持久化会话到客户端数据库 更新本地seqId
2.2.2 流程描述
[前端主进程+渲染进程] 发送流程:
- 用户在渲染进程发送消息,通过IPC通知主进程
- 主进程生成本地序列号(localSeqId,仅用于本地乐观显示)
- 主进程生成唯一客户端消息ID,格式:
clientId = userId-timestamp(用于去重) - 主进程通过WebSocket发送消息到服务器(不等待ACK)
- 渲染进程立即更新UI显示消息,状态为"已发送"
- 主进程的会话管理器立即更新会话(乐观更新):
- 更新会话的最后消息预览
- 更新会话的活跃时间
- 将会话移到会话列表顶部
- 更新会话的sessionMsgSeqId(本地临时值)
- 将会话持久化到客户端数据库
[服务端] 服务器处理:
- 服务器接收消息,分配全局唯一的服务器序列号(serverSeqId)
- 更新后端数据库中该会话的sessionMsgSeqId(覆盖客户端的临时值)
- 更新用户对应的序列号映射
- 持久化消息到后端数据库
- 更新会话的最后消息信息
- 推送消息给接收方用户
- (可选)向发送方返回serverSeqId,更新主进程的本地serverSeqId
[前端主进程] 接收流程:
- 主进程接收服务器推送的消息(包含seqId)
- 消息去重: 使用
senderId-clientId作为唯一标识 - 序列号验证: 检查seqId是否大于本地保存的serverSeqId
- 如果seqId合法:
- 更新本地serverSeqId
- 主进程的会话管理器更新会话信息
- 更新会话的sessionMsgSeqId(从服务器获取的真实seqId)
- 计算未读数: 未读数 = sessionMsgSeqId - msg_seq_id
- 通过IPC通知渲染进程处理并显示消息
- 将会话持久化到客户端数据库
- (可选)向服务器发送同步确认
- 如果seqId <= serverSeqId,忽略过时消息
[前端主进程+服务端] 离线同步:
前端主进程部分:
- 主进程上线后,携带最后同步的seqId请求离线消息
- 批量接收并处理离线消息
- 主进程的会话管理器批量更新会话的sessionMsgSeqId
- 将会话批量持久化到客户端数据库
- 重新计算所有会话的未读数
- 更新本地seqId状态
- 通过IPC通知渲染进程更新UI
服务端部分:
- 服务器在后端数据库中查询seqId大于该值的所有消息
- 返回离线消息列表给主进程
[前端主进程] 未读数计算:
- 会话维护两个序列号:
- msg_seq_id: 用户已读的消息序列号
- sessionMsgSeqId: 会话最新消息的序列号
- 未读数 = sessionMsgSeqId - msg_seq_id
- 标记已读时,更新msg_seq_id为sessionMsgSeqId,未读数清零
- 接收新消息时,sessionMsgSeqId增加,未读数自动增加
2.2.3 优缺点分析
优点:
- 不需要ACK确认,消息发送延迟极低
- 天然支持消息去重,通过seqId可识别重复消息
- 天然支持离线消息同步,基于seqId可精确获取增量消息
- 序列号可用于计算未读消息数,维护简单
- 自动处理乱序消息,只接收seqId更大的消息
- 适合大规模用户场景
缺点:
- 实现复杂度较高,需要维护序列号状态
- 需要消息去重缓存,占用内存(可通过LRU策略优化)
- 服务器需要分配全局序列号,对并发性能有要求
- 序列号机制对消息顺序敏感,设计不当可能导致消息丢失
- 客户端和服务器状态同步需要额外机制
2.2.4 适用场景
- 大规模用户IM应用
- 需要优秀离线消息支持的场景
- 用户经常断线重连的应用
- 需要精确计算未读数的应用
- 消息量较大的社交应用
2.2.5 代码实现
点击展开/收起 [前端主进程] 基于消息序列号的同步机制
typescript
/**
* [伪代码示例]
*
* [前端主进程] 基于消息序列号的同步机制
* 使用seqId实现消息去重和同步
*
* 说明: 该类位于前端主进程中,负责管理本地序列号、消息去重缓存、发送和接收消息、更新会话等
* 通过IPC与渲染进程通信,渲染进程负责UI展示
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 消息类型定义(假设已定义,包含seqId和clientId字段)
// interface Message {
// id: string;
// seqId: number; // 服务器分配的序列号
// clientId: string; // 客户端生成的消息ID
// content: string;
// senderId: string;
// conversationId: string;
// timestamp: number;
// status: 'sending' | 'sent' | 'failed';
// }
class SeqIdMessageManager {
// 本地消息序列号
private localSeqId: number = 0;
// 服务器返回的消息序列号
private serverSeqId: number = 0;
// 消息去重缓存
private messageDedupCache: Set<string> = new Set();
// 用户ID(实际应从用户上下文获取)
private userId: string = 'current-user-id';
// 发送消息
async sendMessage(message: Message): Promise<void> {
// 生成本地临时序列号(仅用于乐观显示,会被服务器返回的seqId覆盖)
const localSeq = ++this.localSeqId;
// 生成唯一客户端消息ID(用于去重)
message.clientId = `${this.userId}-${Date.now()}`;
// 发送到服务器(不等待ACK)
this.sendToServer(message);
// 更新消息状态为已发送
message.status = 'sent';
// 通过IPC通知渲染进程立即更新UI显示消息
this.notifyRenderProcess('message:sent', { message });
// 立即更新会话(使用本地临时seqId)
await this.updateSessionImmediate(message, localSeq);
}
// 接收消息
handleIncomingMessage(message: Message) {
// 消息去重
const messageKey = `${message.senderId}-${message.clientId}`;
if (this.messageDedupCache.has(messageKey)) {
console.log('重复消息,忽略');
return;
}
this.messageDedupCache.add(messageKey);
// 检查消息序列号是否合法(seqId由服务器分配)
if (message.seqId <= this.serverSeqId) {
console.log('过时消息,忽略');
return;
}
// 更新服务器序列号(仅用于接收方)
this.serverSeqId = message.seqId;
// 处理消息
this.processMessage(message);
// 同步到服务器(可选)
this.syncToServer({ lastSeqId: this.serverSeqId });
}
// 接收服务器返回的seqId(用于发送方)
handleServerSeqResponse(messageId: string, serverSeqId: number) {
// 找到对应的消息,更新其seqId为服务器返回的值
const message = this.findMessageById(messageId);
if (message) {
message.seqId = serverSeqId;
// 更新本地serverSeqId
this.serverSeqId = serverSeqId;
}
}
// 立即更新会话(使用本地临时seqId)
private async updateSessionImmediate(message: Message, localSeqId: number) {
// 立即更新会话的最后一条消息(使用本地临时seqId,后续会被服务器返回的seqId覆盖)
await this.updateSession({
sessionId: message.conversationId,
lastMessage: message,
lastMessageTime: message.timestamp,
activeAt: message.timestamp,
sessionMsgSeqId: localSeqId // 临时使用本地seqId
});
// 计算未读数: 未读数 = sessionMsgSeqId - msg_seq_id
const unreadCount = this.calculateUnreadCount(message.conversationId);
// 通过IPC通知渲染进程更新未读数
this.notifyRenderProcess('session:unread-updated', {
sessionId: message.conversationId,
unreadCount
});
// 将会话持久化到客户端数据库(实际实现需要保存到数据库)
console.log('持久化会话到客户端数据库:', message.conversationId);
}
// 计算未读数
private calculateUnreadCount(sessionId: string): number {
const session = this.getSessionFromCache(sessionId);
if (!session) return 0;
// 未读数 = sessionMsgSeqId - msg_seq_id
return session.sessionMsgSeqId - session.msgSeqId;
}
// 离线同步 - 上线时请求离线消息
async syncOfflineMessages(): Promise<void> {
// 携带最后同步的seqId请求离线消息
const lastSeqId = this.serverSeqId;
const offlineMessages = await this.requestOfflineMessages(lastSeqId);
// 批量接收并处理离线消息
for (const message of offlineMessages) {
this.handleIncomingMessage(message);
}
// 主进程的会话管理器批量更新会话的sessionMsgSeqId
await this.batchUpdateSessions(offlineMessages);
// 重新计算所有会话的未读数
await this.recalculateAllUnreadCounts();
// 更新本地seqId状态
if (offlineMessages.length > 0) {
const lastMessage = offlineMessages[offlineMessages.length - 1];
this.serverSeqId = lastMessage.seqId;
}
// 通过IPC通知渲染进程更新UI
this.notifyRenderProcess('offline-sync:completed', {
messageCount: offlineMessages.length
});
}
// 请求离线消息(实际实现需要通过WebSocket或HTTP请求服务器)
private async requestOfflineMessages(sinceSeqId: number): Promise<Message[]> {
console.log(`请求离线消息,从seqId ${sinceSeqId} 之后`);
// 实际实现需要发送同步请求到服务器
return [];
}
// 批量更新会话
private async batchUpdateSessions(messages: Message[]): Promise<void> {
// 将会话批量持久化到客户端数据库
for (const message of messages) {
await this.updateSession({
sessionId: message.conversationId,
lastMessage: message,
lastMessageTime: message.timestamp,
activeAt: message.timestamp,
sessionMsgSeqId: message.seqId
});
}
console.log('批量持久化会话到客户端数据库');
}
// 重新计算所有会话的未读数
private async recalculateAllUnreadCounts(): Promise<void> {
const sessions = await this.getAllSessions();
for (const session of sessions) {
const unreadCount = session.sessionMsgSeqId - session.msgSeqId;
this.notifyRenderProcess('session:unread-updated', {
sessionId: session.id,
unreadCount
});
}
console.log('重新计算所有会话的未读数');
}
// 发送消息到服务器的方法(需要根据实际网络层实现)
private sendToServer(message: Message): void {
// 实际实现需要处理WebSocket发送等
console.log('发送消息到服务器:', message);
}
// 处理消息的方法(实际业务逻辑)
private processMessage(message: Message): void {
// 实际实现可能包括:保存消息到本地数据库、通过IPC通知渲染进程更新UI等
this.notifyRenderProcess('message:received', { message });
console.log('处理消息:', message);
}
// 同步到服务器的方法
private syncToServer(syncData: { lastSeqId: number }): void {
// 实际实现需要发送同步请求到服务器
console.log('同步到服务器:', syncData);
}
// 根据消息ID查找消息(实际实现需要查询本地数据库)
private findMessageById(messageId: string): Message | null {
// 实际实现需要从本地数据库查询
console.log('查找消息:', messageId);
return null;
}
// 更新会话的方法(实际实现需要更新本地会话存储)
private async updateSession(sessionData: {
sessionId: string;
lastMessage: Message;
lastMessageTime: number;
activeAt: number;
sessionMsgSeqId: number;
}): Promise<void> {
// 实际实现需要更新本地会话
console.log('更新会话:', sessionData);
}
// 从会话缓存获取会话
private getSessionFromCache(sessionId: string): any {
// 实际实现需要从会话缓存或数据库获取
console.log('获取会话:', sessionId);
return null;
}
// 获取所有会话
private async getAllSessions(): Promise<any[]> {
// 实际实现需要从数据库获取所有会话
console.log('获取所有会话');
return [];
}
// 通过IPC通知渲染进程
private notifyRenderProcess(event: string, data: any): void {
// 实际实现需要通过 electron 的 webContents.send 发送消息到渲染进程
console.log(`通知渲染进程 [${event}]:`, data);
}
}
点击展开/收起 [后端服务器] 服务器端序列号管理 & 服务器端会话管理
typescript
/**
* [伪代码示例]
*
* [后端服务器] 服务器端序列号管理
*
* 说明: 该类位于后端服务器,负责分配全局序列号、处理客户端消息、同步检查点、推送消息等
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
class SeqIdServerHandler {
// 用户会话的序列号映射(记录每个用户在各会话中最后接收的seqId)
private userSeqMap: Map<string, number> = new Map();
// 处理客户端消息
async handleClientMessage(client: WebSocket, message: Message) {
// 分配全局唯一序列号
const serverSeqId = await this.allocateServerSeqId();
message.seqId = serverSeqId;
// 更新用户序列号
this.userSeqMap.set(message.senderId, serverSeqId);
// 存储消息
await this.saveMessage(message);
// 推送给接收方(消息携带服务器分配的seqId)
await this.pushToRecipients(message);
// 返回服务器序列号给发送方(可选,用于同步本地状态)
return { seqId: serverSeqId, messageId: message.id };
}
// 同步检查点
async handleSync(userId: string, lastSeqId: number) {
// 查找用户从lastSeqId之后的所有消息
const messages = await this.getMessagesAfterSeqId(userId, lastSeqId);
// 发送增量同步消息
return messages;
}
// 分配全局唯一序列号(实际实现需要考虑分布式场景)
private async allocateServerSeqId(): Promise<number> {
// 实际实现可能使用Redis原子递增、数据库序列等
console.log('分配全局序列号');
return Date.now(); // 简化示例,使用时间戳
}
// 保存消息到数据库
private async saveMessage(message: Message): Promise<void> {
// 实际实现需要持久化消息到数据库
console.log('保存消息到数据库:', message);
}
// 推送给接收方
private async pushToRecipients(message: Message): Promise<void> {
// 实际实现需要查找目标用户的连接并推送消息
console.log('推送消息给接收方:', message);
}
// 获取用户从指定序列号之后的消息
private async getMessagesAfterSeqId(userId: string, lastSeqId: number): Promise<Message[]> {
// 实际实现需要查询数据库,获取seqId > lastSeqId的消息
console.log(`查询用户${userId}从seqId ${lastSeqId}之后的消息`);
return []; // 简化示例,返回空数组
}
}
typescript
/**
* [伪代码示例]
*
* [后端服务器] 服务器端会话管理 - 序列号方案
*
* 说明: 该类位于后端服务器,负责处理消息时更新会话、处理离线同步请求、持久化会话等
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 会话类型定义(假设已定义,包含序列号字段)
// interface Session {
// id: string;
// participants: string[];
// msg_seq_id: number; // 用户已读的消息序列号
// sessionMsgSeqId: number; // 会话最新消息的序列号
// lastMessageId: string;
// lastMessageContent: string;
// lastMessageTime: number;
// createdAt: Date;
// updatedAt: Date;
// }
class ServerSessionManagerWithSeq {
/**
* 处理消息时更新会话
* 注意: 这个方法在消息已分配seqId后被调用
*/
async updateSessionOnMessage(message: Message): Promise<void> {
// 1. 检查会话是否存在
let session = await this.getSessionById(message.conversationId);
if (!session) {
// 2. 会话不存在,创建会话
session = await this.createSession({
id: message.conversationId,
participants: message.participants,
msg_seq_id: 0,
sessionMsgSeqId: 0,
createdAt: new Date()
});
}
// 3. 使用已分配的服务器序列号更新会话
session.sessionMsgSeqId = message.seqId;
session.lastMessageId = message.id;
session.lastMessageContent = message.content;
session.lastMessageTime = message.timestamp;
session.updatedAt = new Date();
// 4. 持久化到数据库
await this.saveSession(session);
// 5. 更新用户序列号映射
await this.updateUserSeqMap(message.senderId, message.seqId);
}
/**
* 处理离线同步请求
*/
async handleOfflineSync(userId: string, lastSeqId: number): Promise<Message[]> {
// 查询seqId > lastSeqId的所有消息
const messages = await this.getMessagesAfterSeqId(userId, lastSeqId);
// 批量更新会话信息(注意:这些消息已经分配了seqId)
for (const message of messages) {
await this.updateSessionOnMessage(message);
}
return messages;
}
// 根据会话ID获取会话(实际实现需要数据库查询)
private async getSessionById(sessionId: string): Promise<Session | null> {
// 实际实现需要从数据库查询
console.log('查询会话:', sessionId);
return null; // 假设不存在
}
// 创建新会话
private async createSession(sessionData: {
id: string;
participants: string[];
msg_seq_id: number;
sessionMsgSeqId: number;
createdAt: Date;
}): Promise<Session> {
// 实际实现需要创建会话并保存到数据库
console.log('创建会话:', sessionData);
return {
...sessionData,
lastMessageId: '',
lastMessageContent: '',
lastMessageTime: 0,
updatedAt: new Date()
};
}
// 保存会话到数据库
private async saveSession(session: Session): Promise<void> {
// 实际实现需要持久化到数据库
console.log('保存会话到数据库:', session);
}
// 更新用户序列号映射
private async updateUserSeqMap(userId: string, seqId: number): Promise<void> {
// 实际实现需要更新用户的序列号状态
console.log(`更新用户${userId}的序列号映射为: ${seqId}`);
}
// 获取用户从指定序列号之后的消息(与SeqIdServerHandler中的方法类似)
private async getMessagesAfterSeqId(userId: string, lastSeqId: number): Promise<Message[]> {
// 实际实现需要查询数据库
console.log(`查询用户${userId}从seqId ${lastSeqId}之后的消息`);
return [];
}
}
2.3 方案三: 混合队列 + 心跳确认方案
2.3.1 流程图
心跳管理器 服务器 后端数据库 WebSocket 客户端数据库 发送队列 会话管理器 UI层 用户 心跳管理器 服务器 后端数据库 WebSocket 客户端数据库 发送队列 会话管理器 UI层 用户 发送消息流程 alt [会话不存在] 发送失败重试 指数退避延迟 alt [重试次数 < 最大重试次数] [重试次数 >= 最大重试次数] alt [发送失败] 心跳批量确认流程 loop [每隔30秒] 离线消息处理 发送消息 生成消息ID 检查会话是否存在 创建会话 返回会话ID 乐观更新UI (立即显示) 立即更新会话 (乐观更新) 更新最后消息 更新活跃时间 移动会话到顶部 防抖持久化会话到客户端数据库 加入发送队列 标记为未确认 触发队列处理 发送消息到服务器 持久化消息 更新会话最后消息 返回成功 更新未确认消息状态 从未确认列表移除 重试次数++ 重新发送 (降低优先级) 标记消息为"失败" 回滚会话状态(可选) 持久化回滚到客户端数据库 从队列移除 发送心跳包 批量确认未确认消息 检查消息是否存在 批量更新会话状态 返回已确认的消息ID列表 批量移除已确认消息 同步会话状态 批量持久化会话到客户端数据库 离线 (网络断开) 消息堆积在队列 会话状态临时保存 重新上线 批量发送堆积消息 持久化消息 批量更新会话 返回成功 最终确认会话状态 批量持久化会话到客户端数据库 清空队列
2.3.2 流程描述
[前端主进程+渲染进程] 发送流程(乐观更新):
- 用户在渲染进程发送消息,通过IPC通知主进程
- 渲染进程UI层立即生成消息ID
- 渲染进程UI层检查会话是否存在(防止发送到不存在的会话)
- 如果会话不存在:
- 立即创建新会话
- 渲染进程乐观更新UI: 立即在聊天界面显示消息
- 主进程的会话管理器立即更新会话(乐观更新):
- 更新会话的最后消息预览
- 更新会话的活跃时间
- 将会话移到会话列表顶部
- 防抖持久化会话数据到客户端数据库(延迟1秒写入,避免频繁写入)
- 主进程加入发送队列: 按优先级排序(文本消息优先级高,媒体消息优先级低)
- 主进程标记为未确认: 将消息ID加入未确认消息集合
- 主进程触发队列处理: 后台异步发送消息到服务器
[前端主进程] 队列处理:
- 主进程从队列中按优先级取出消息
- 主进程通过WebSocket发送到服务器
- 如果发送成功:
- 从未确认集合中移除该消息
- 更新消息状态为"已发送",通过IPC通知渲染进程
- 主进程的会话管理器更新未确认消息状态
- 如果发送失败:
- 递增重试次数,降低优先级,重新入队
- 如果重试次数超过上限(默认3次),标记为失败,通过IPC通知渲染进程
- (可选)回滚会话状态(如果消息是会话的最后一条)
- 将回滚的会话状态持久化到客户端数据库
[前端主进程+服务端] 心跳批量确认:
前端主进程部分:
- 每隔固定时间(如30秒)发送心跳包
- 心跳时将所有未确认消息的ID发送到服务器
- 接收服务器返回的已确认消息ID列表
- 主进程批量从未确认集合中移除这些消息
- 主进程的会话管理器同步会话状态(确保本地会话与服务器一致)
- 将同步后的会话状态批量持久化到客户端数据库
- 通过IPC通知渲染进程更新UI
服务端部分:
- 接收主进程发送的未确认消息ID列表
- 批量检查这些消息是否已持久化到后端数据库
- 服务器批量更新后端数据库中的会话状态
- 返回已确认的消息ID列表
[前端主进程+服务端] 离线消息处理:
前端主进程部分:
- 网络断开时,消息继续堆积在发送队列
- 主进程的会话管理器临时保存会话状态(不持久化到数据库)
- 网络恢复后,队列自动处理堆积的消息
- 按优先级顺序批量发送到服务器
- 主进程的会话管理器最终确认会话状态
- 将最终确认的会话状态批量持久化到客户端数据库
- 通过IPC通知渲染进程更新UI
- 发送完成后清空队列
服务端部分:
- 接收主进程批量发送的消息
- 批量持久化消息到后端数据库并更新会话
- 返回成功确认
2.3.3 优缺点分析
优点:
- 用户体验极佳: 消息即时显示,无感知延迟
- 减少网络交互: 心跳时批量确认,降低网络开销
- 支持离线消息堆积: 网络恢复后自动重试
- 支持消息优先级: 重要消息优先发送
- 乐观更新: 用户感知不到发送延迟
- 队列机制: 支持网络波动场景
- 防抖持久化: 减少数据库写入频率
缺点:
- 可能出现消息丢失风险: 如果队列未持久化
- 乐观更新可能导致UI与实际不同步: 需要消息状态更新机制
- 需要额外的同步机制: 心跳批量确认
- 队列管理增加复杂度: 优先级、重试、去重
- 内存占用: 队列占用内存,需要限制大小
2.3.4 适用场景
- 移动端IM应用(网络不稳定)
- 对用户体验要求极高的应用
- 消息量中等,不需要MQ的场景
- 支持离线消息的场景
- 需要消息优先级的场景
2.3.5 代码实现
点击展开/收起 [前端主进程] 混合队列 + 心跳确认方案
typescript
/**
* [伪代码示例]
*
* [前端主进程] 混合队列 + 心跳确认方案
* 发送时立即更新UI,后台队列异步重试,心跳时批量确认
*
* 说明: 该类位于前端主进程中,负责管理发送队列、未确认消息、优先级队列、心跳批量确认等
* 通过IPC与渲染进程通信,渲染进程负责UI展示
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 消息类型定义(假设已定义)
// interface Message {
// id: string;
// content: string;
// senderId: string;
// conversationId: string;
// timestamp: number;
// status: 'sending' | 'sent' | 'failed';
// priority: number; // 消息优先级
// retryCount: number; // 重试次数
// }
// 会话管理器接口(假设已定义)
// interface SessionManager {
// updateSessionImmediate(message: Message): Promise<void>;
// rollbackSessionState(sessionId: string, lastMessage: Message): Promise<void>;
// syncSessionsOnHeartbeat(): Promise<void>;
// }
// 优先级队列实现(简化示例)
class PriorityQueue<T> {
private items: {item: T, priority: number}[] = [];
enqueue(item: T, priority: number): void {
this.items.push({item, priority});
this.items.sort((a, b) => b.priority - a.priority); // 优先级高的在前
}
dequeue(): T {
return this.items.shift()!.item;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
class HybridQueueManager {
// 发送队列: 按优先级排序
private sendQueue: PriorityQueue<Message> = new PriorityQueue();
// 已发送但未确认的消息
private unconfirmedMessages: Map<string, Message> = new Map();
// 心跳定时器
private heartbeatTimer: NodeJS.Timeout | null = null;
// 网络状态
private isOnline: boolean = true;
// 临时会话状态缓存(离线时使用)
private tempSessionState: Map<string, any> = new Map();
// 配置信息
private config = {
maxRetry: 3, // 最大重试次数
heartbeatInterval: 30000 // 心跳间隔(毫秒)
};
// 会话管理器(实际应从外部注入)
private sessionManager: SessionManager;
constructor(sessionManager: SessionManager) {
this.sessionManager = sessionManager;
// 启动心跳定时器
this.startHeartbeat();
}
/**
* 发送消息
*/
async sendMessage(message: Message): Promise<void> {
// 1. 渲染进程UI层立即生成消息ID (通过IPC从渲染进程接收)
// message.id 已在渲染进程中生成
// 2. 渲染进程UI层检查会话是否存在
const sessionExists = await this.checkSessionExists(message.conversationId);
if (!sessionExists) {
// 3. 如果会话不存在,立即创建新会话
await this.createSession(message.conversationId);
}
// 4. 渲染进程乐观更新UI
// 通过IPC通知渲染进程立即在聊天界面显示消息
await this.updateMessageInUI(message);
// 5. 主进程的会话管理器立即更新会话(乐观更新)
await this.sessionManager.updateSessionImmediate(message);
// 6. 防抖持久化会话数据到客户端数据库 (延迟1秒写入,避免频繁写入)
this.debouncedPersistSession(message.conversationId);
// 7. 主进程加入发送队列: 按优先级排序
this.sendQueue.enqueue(message, message.priority);
// 8. 主进程标记为未确认: 将消息ID加入未确认消息集合
this.unconfirmedMessages.set(message.id, message);
// 9. 主进程触发队列处理: 后台异步发送消息到服务器
this.processQueue();
}
/**
* 处理队列
*/
private async processQueue() {
// 如果离线,不处理队列
if (!this.isOnline) return;
while (!this.sendQueue.isEmpty()) {
const message = this.sendQueue.dequeue();
try {
// 1. 主进程通过WebSocket发送到服务器
await this.sendToServer(message);
// 2. 发送成功,从未确认集合中移除该消息
this.unconfirmedMessages.delete(message.id);
// 3. 更新消息状态为"已发送",通过IPC通知渲染进程
await this.updateMessageStatus(message.id, 'sent');
// 4. 主进程的会话管理器更新未确认消息状态
await this.sessionManager.updateUnconfirmedMessages(message.conversationId);
} catch (error) {
// 1. 递增重试次数,降低优先级,重新入队
message.retryCount++;
// 2. 如果重试次数超过上限(默认3次),标记为失败,通过IPC通知渲染进程
if (message.retryCount < this.config.maxRetry) {
this.sendQueue.enqueue(message, message.priority - 1); // 降低优先级
} else {
// 1. 标记为失败,通过IPC通知渲染进程
message.status = 'failed';
this.unconfirmedMessages.delete(message.id);
await this.updateMessageStatus(message.id, 'failed');
// 2. (可选)回滚会话状态(如果消息是会话的最后一条)
// 查询当前会话的最后消息,如果与当前消息相同则回滚
const session = await this.getSession(message.conversationId);
if (session && session.lastMessageId === message.id) {
await this.sessionManager.rollbackSessionState(
message.conversationId,
session.lastMessage
);
// 3. 将回滚的会话状态持久化到客户端数据库
console.log('回滚会话状态并持久化到客户端数据库');
}
}
}
}
}
/**
* 心跳批量确认
*/
private async heartbeatConfirm() {
if (this.unconfirmedMessages.size === 0) return;
// 1. 心跳时将所有未确认消息的ID发送到服务器
const confirmIds = Array.from(this.unconfirmedMessages.keys());
// 2. 接收服务器返回的已确认消息ID列表
const confirmedIds = await this.batchConfirm(confirmIds);
// 3. 主进程批量从未确认集合中移除这些消息
confirmedIds.forEach(id => {
this.unconfirmedMessages.delete(id);
});
// 4. 主进程的会话管理器同步会话状态(确保本地会话与服务器一致)
await this.sessionManager.syncSessionsOnHeartbeat();
// 5. 将同步后的会话状态批量持久化到客户端数据库
console.log('批量持久化会话状态到客户端数据库');
// 6. 通过IPC通知渲染进程更新UI
await this.notifyRenderProcess('heartbeat:sync-completed', {
confirmedCount: confirmedIds.length
});
}
/**
* 启动心跳定时器
*/
private startHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
this.heartbeatTimer = setInterval(() => {
this.heartbeatConfirm();
}, this.config.heartbeatInterval);
console.log('启动心跳定时器,间隔:', this.config.heartbeatInterval);
}
/**
* 检查会话是否存在
*/
private async checkSessionExists(sessionId: string): Promise<boolean> {
const session = await this.getSession(sessionId);
return session !== null;
}
/**
* 创建会话
*/
private async createSession(sessionId: string): Promise<void> {
// 通过IPC通知渲染进程创建会话
await this.notifyRenderProcess('session:create', { sessionId });
console.log('创建新会话:', sessionId);
}
/**
* 防抖持久化会话
*/
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
private debouncedPersistSession(sessionId: string): void {
// 清除之前的定时器
const existingTimer = this.debounceTimers.get(sessionId);
if (existingTimer) {
clearTimeout(existingTimer);
}
// 设置新的定时器(延迟1秒写入)
const timer = setTimeout(() => {
this.persistSession(sessionId);
this.debounceTimers.delete(sessionId);
}, 1000);
this.debounceTimers.set(sessionId, timer);
}
/**
* 持久化会话
*/
private async persistSession(sessionId: string): Promise<void> {
const session = await this.getSession(sessionId);
if (session) {
// 实际实现需要保存会话到数据库
console.log('防抖持久化会话到客户端数据库:', sessionId);
}
}
// 更新消息UI的方法(实际实现需要更新界面显示)
private async updateMessageInUI(message: Message): Promise<void> {
console.log('更新消息UI:', message);
}
// 发送消息到服务器的方法(需要根据实际网络层实现)
private async sendToServer(message: Message): Promise<void> {
console.log('发送消息到服务器:', message);
}
// 批量确认的方法(需要与服务器交互)
private async batchConfirm(messageIds: string[]): Promise<string[]> {
console.log('批量确认消息:', messageIds);
// 实际实现需要发送请求到服务器,服务器返回已确认的消息ID列表
return messageIds; // 简化示例,假设所有消息都已确认
}
// 获取会话的方法(实际实现需要查询本地数据库)
private async getSession(sessionId: string): Promise<any> {
console.log('获取会话:', sessionId);
return null;
}
/**
* 处理网络状态变化
*/
onNetworkStatusChange(online: boolean): void {
if (online) {
// 1. 网络恢复后,队列自动处理堆积的消息
this.isOnline = true;
// 2. 恢复临时保存的会话状态
this.restoreSessionStates();
// 3. 队列自动处理堆积的消息
this.processQueue();
console.log('网络恢复,开始处理堆积的消息');
} else {
// 1. 网络断开时,消息继续堆积在发送队列
this.isOnline = false;
// 2. 主进程的会话管理器临时保存会话状态(不持久化到数据库)
this.saveTempSessionStates();
console.log('网络断开,临时保存会话状态');
}
}
/**
* 临时保存会话状态
*/
private saveTempSessionStates(): void {
const sessions = this.getActiveSessions();
for (const session of sessions) {
this.tempSessionState.set(session.id, { ...session });
console.log('临时保存会话状态:', session.id);
}
}
/**
* 恢复会话状态
*/
private async restoreSessionStates(): Promise<void> {
for (const [sessionId, state] of this.tempSessionState) {
// 恢复会话状态
await this.sessionManager.updateSessionImmediate({
...state,
restored: true
});
// 5. 主进程的会话管理器最终确认会话状态
console.log('恢复会话状态:', sessionId);
// 6. 将最终确认的会话状态批量持久化到客户端数据库
await this.persistSession(sessionId);
}
// 7. 发送完成后清空临时状态
this.tempSessionState.clear();
}
/**
* 获取活跃会话
*/
private getActiveSessions(): any[] {
// 实际实现需要从会话管理器获取活跃会话
return [];
}
/**
* 更新消息状态
*/
private async updateMessageStatus(messageId: string, status: 'sent' | 'failed'): Promise<void> {
// 通过IPC通知渲染进程更新消息状态
await this.notifyRenderProcess('message:status-updated', {
messageId,
status
});
console.log(`更新消息状态: ${messageId} -> ${status}`);
}
/**
* 销毁管理器
*/
destroy(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
// 清理防抖定时器
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
console.log('销毁混合队列管理器');
}
}
点击展开/收起 [后端服务器] 服务器批量确认处理 & 服务器端会话管理
typescript
/**
* [伪代码示例]
*
* [后端服务器] 服务器批量确认处理
*
* 说明: 该类位于后端服务器,负责批量确认消息、检查消息是否存在等
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
class BatchConfirmHandler {
/**
* 批量确认消息
*/
async batchConfirm(messageIds: string[]): Promise<string[]> {
const confirmedIds: string[] = [];
for (const messageId of messageIds) {
// 1. 检查消息是否存在
const exists = await this.messageExists(messageId);
if (exists) {
confirmedIds.push(messageId);
}
}
// 2. 服务器批量更新后端数据库中的会话状态
if (confirmedIds.length > 0) {
await this.batchUpdateSessions(confirmedIds);
}
// 3. 返回已确认的消息ID列表
return confirmedIds;
}
/**
* 批量更新会话(批量确认时调用)
*/
private async batchUpdateSessions(messageIds: string[]): Promise<void> {
// 批量查询这些消息所属的会话
const sessionsToUpdate = await this.getSessionsByMessageIds(messageIds);
// 批量更新会话状态
for (const session of sessionsToUpdate) {
session.updatedAt = new Date();
// 实际实现需要持久化到数据库
console.log('批量更新会话状态:', session.id);
}
}
/**
* 根据消息ID获取会话列表
*/
private async getSessionsByMessageIds(messageIds: string[]): Promise<any[]> {
// 实际实现需要查询数据库,获取消息对应的会话
console.log('批量查询消息对应的会话');
return [];
}
/**
* 处理离线消息
*/
async handleOfflineMessages(messages: Message[]): Promise<void> {
// 1. 批量持久化消息到后端数据库并更新会话
for (const message of messages) {
await this.saveMessage(message);
await this.updateSessionOnMessage(message);
}
console.log(`批量处理离线消息: ${messages.length} 条`);
}
/**
* 保存消息到数据库
*/
private async saveMessage(message: Message): Promise<void> {
// 实际实现需要持久化消息到数据库
console.log('保存消息到数据库:', message.id);
}
/**
* 检查消息是否存在的方法(实际实现需要查询数据库)
*/
private async messageExists(messageId: string): Promise<boolean> {
// 实际实现需要查询数据库,检查消息是否已持久化
console.log('检查消息是否存在:', messageId);
return true; // 简化示例,假设消息都存在
}
}
/**
* [伪代码示例]
*
* [后端服务器] 服务器端会话管理 - 混合队列方案
*
* 说明: 该类位于后端服务器,负责处理消息时更新会话、批量更新会话、处理离线消息同步等
*
* 注意: 这是简化示例,实际实现需要考虑更多边界情况
*/
// 会话类型定义(假设已定义)
// interface Session {
// id: string;
// participants: string[];
// lastMessageId: string;
// lastMessageContent: string;
// lastMessageTime: number;
// unreadCount: number;
// createdAt: Date;
// updatedAt: Date;
// }
class ServerSessionManagerWithQueue {
/**
* 处理消息时更新会话
*/
async updateSessionOnMessage(message: Message): Promise<void> {
// 1. 检查会话是否存在
let session = await this.getSessionById(message.conversationId);
if (!session) {
// 2. 会话不存在,创建会话
session = await this.createSession({
id: message.conversationId,
participants: message.participants,
createdAt: new Date()
});
}
// 3. 更新会话的最后消息
session.lastMessageId = message.id;
session.lastMessageContent = message.content;
session.lastMessageTime = message.timestamp;
session.updatedAt = new Date();
// 4. 持久化到数据库
await this.saveSession(session);
}
/**
* 批量更新会话(心跳时调用)
*/
async batchUpdateSessions(messageIds: string[]): Promise<void> {
// 1. 接收主进程发送的未确认消息ID列表
// 2. 批量检查这些消息是否已持久化到后端数据库
// 3. 服务器批量更新后端数据库中的会话状态
// 4. 返回已确认的消息ID列表
// 批量查询消息
const messages = await this.batchGetMessages(messageIds);
// 按会话分组
const sessionsToUpdate = new Map<string, Message[]>();
for (const message of messages) {
if (!sessionsToUpdate.has(message.conversationId)) {
sessionsToUpdate.set(message.conversationId, []);
}
sessionsToUpdate.get(message.conversationId)!.push(message);
}
// 批量更新会话
for (const [sessionId, sessionMessages] of sessionsToUpdate) {
const lastMessage = sessionMessages[sessionMessages.length - 1];
await this.updateSessionOnMessage(lastMessage);
}
}
/**
* 处理离线消息同步
*/
async handleOfflineMessages(userId: string, messages: Message[]): Promise<void> {
// 1. 接收主进程批量发送的消息
// 2. 批量持久化消息到后端数据库并更新会话
// 3. 返回成功确认
// 按会话分组消息
const messagesBySession = this.groupMessagesBySession(messages);
// 批量更新会话
for (const [sessionId, sessionMessages] of messagesBySession) {
const lastMessage = sessionMessages[sessionMessages.length - 1];
// 更新会话
await this.updateSessionOnMessage(lastMessage);
// 更新未读数
const session = await this.getSessionById(sessionId);
if (session) {
session.unreadCount += sessionMessages.length;
await this.saveSession(session);
}
}
console.log(`批量处理离线消息: ${messages.length} 条,涉及 ${messagesBySession.size} 个会话`);
}
// 根据会话ID获取会话(实际实现需要数据库查询)
private async getSessionById(sessionId: string): Promise<Session | null> {
// 实际实现需要从数据库查询
console.log('查询会话:', sessionId);
return null; // 假设不存在
}
// 创建新会话
private async createSession(sessionData: {
id: string;
participants: string[];
createdAt: Date;
}): Promise<Session> {
// 实际实现需要创建会话并保存到数据库
console.log('创建会话:', sessionData);
return {
...sessionData,
lastMessageId: '',
lastMessageContent: '',
lastMessageTime: 0,
unreadCount: 0,
updatedAt: new Date()
};
}
// 保存会话到数据库
private async saveSession(session: Session): Promise<void> {
// 实际实现需要持久化到数据库
console.log('保存会话到数据库:', session);
}
// 批量获取消息(实际实现需要数据库查询)
private async batchGetMessages(messageIds: string[]): Promise<Message[]> {
// 实际实现需要批量查询消息
console.log('批量获取消息:', messageIds);
return [];
}
// 按会话分组消息(工具方法)
private groupMessagesBySession(messages: Message[]): Map<string, Message[]> {
const groups = new Map<string, Message[]>();
for (const message of messages) {
const sessionId = message.conversationId;
if (!groups.has(sessionId)) {
groups.set(sessionId, []);
}
groups.get(sessionId)!.push(message);
}
return groups;
}
}
三、方案优缺点对比
3.1 评分标准
评分说明: 使用1-5分制评分,分数越高表示该项性能越好(实现复杂度和运维成本除外,分数越高表示越简单/成本越低)
| 评分标准 | 说明 |
|---|---|
| 5分 | 极好/极低/优秀 |
| 4分 | 很好/较低/良好 |
| 3分 | 中等/一般 |
| 2分 | 较差/较高 |
| 1分 | 极差/极高 |
3.2 详细对比评分
| 对比维度 | 评分依据 | 方案一: ACK确认 | 方案二: 序列号同步 | 方案三: 混合队列 |
|---|---|---|---|---|
| 消息可靠性 | 消息送达率、持久化机制、重试能力 | 5/5 (ACK+重试,100%确认) | 4/5 (seqId+去重,高可靠性) | 3/5 (乐观更新,可能丢失) |
| 发送延迟 | 从发送到显示的时间、用户感知 | 3/5 (需等待ACK,200-500ms) | 5/5 (不等待ACK,<50ms) | 5/5 (乐观更新,<50ms) |
| 实现复杂度 | 代码量、维护难度、学习成本 | 4/5 (简单直观,易于调试) | 2/5 (序列号管理、去重复杂) | 3/5 (队列管理、中等复杂度) |
| 网络开销 | 消息确认次数、带宽消耗 | 2/5 (每条消息ACK,开销大) | 4/5 (少量同步,开销低) | 4/5 (批量确认,开销低) |
| 用户体验 | 消息即时性、流畅度、交互体验 | 4/5 (需等待ACK,体验良好) | 5/5 (即时性极佳,体验极好) | 5/5 (即时性极佳,体验极好) |
| 离线支持 | 离线消息同步、断线重连能力 | 2/5 (依赖服务器,支持差) | 5/5 (seqId同步,支持优秀) | 4/5 (本地队列,支持良好) |
| 消息去重 | 去重机制完善度、自动去重能力 | 3/5 (需额外实现去重) | 5/5 (seqId天然去重) | 3/5 (需额外实现去重) |
| 运维成本 | 服务器成本、人力维护成本 | 5/5 (无额外组件,成本低) | 4/5 (无额外组件,成本低) | 4/5 (无额外组件,成本低) |
| 消息乱序 | 乱序处理能力、顺序保证 | 3/5 (重试可能乱序) | 5/5 (seqId自动处理) | 4/5 (队列保证顺序) |
3.3 综合评分
| 方案 | 可靠性 | 延迟 | 复杂度 | 网络开销 | 体验 | 离线 | 去重 | 运维 | 乱序 | 总分 | 适用场景 | 核心特点 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 方案一: ACK确认 | 5 | 3 | 4 | 2 | 4 | 2 | 3 | 5 | 3 | 31/45 | 企业级IM、金融IM | 每条消息ACK确认,可靠性高 |
| 方案二: 序列号同步 | 4 | 5 | 2 | 4 | 5 | 5 | 5 | 4 | 5 | 39/45 | 大规模用户IM、Electron桌面IM | 基于seqId去重,支持离线同步 |
| 方案三: 混合队列 | 3 | 5 | 3 | 4 | 5 | 4 | 3 | 4 | 4 | 35/45 | 移动端IM | 乐观更新UI,后台队列重试 |
免责声明
- 技术文档性质:本文档为技术方案设计文档,内容基于通用技术实践和业界最佳实践编写
- 内容声明:文档中的技术方案、架构设计、代码示例等内容均为通用技术实现,不涉及任何特定公司或项目的商业机密、专利技术或内部架构
- 参考性质:本文档仅供技术参考和学习使用,不构成任何商业建议或技术实施承诺
- 使用风险:读者应根据自身项目的具体需求对本文档内容进行调整和优化,作者不对因使用本文档内容而造成的任何直接或间接损失承担责任
版权声明
本文档内容为原创技术文档,仅供学习交流使用。文档中的代码示例、架构设计等技术内容为通用技术实践,不涉及任何特定公司的商业机密。如需引用本文档内容,请注明出处。