# V-IM **Repository Path**: spring-clou_admin/V-IM ## Basic Information - **Project Name**: V-IM - **Description**: V-IM(中文名:乐聊)基于JS的超轻量级聊天软件。前端:vue3.0、element plus、electron、TypeScrip,支持windows、linux、mac、安卓、IOS、小程序、H5。支持语音消息,视频通话等。 服务端: springboot、tio、mybatis 等技术。 - **Primary Language**: JavaScript - **License**: AGPL-3.0 - **Default Branch**: 2025 - **Homepage**: https://gitee.com/alyouge/V-IM - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1584 - **Created**: 2025-12-25 - **Last Updated**: 2026-01-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # V-IM & V-IM PRO | 企业级即时通讯解决方案 ## 📋 项目概述 V-IM是一款基于WebSocket的企业级即时通讯解决方案,提供稳定、高效、安全的即时通讯服务。支持单聊、群聊、多种消息类型以及完善的用户和组织管理功能,适用于企业内部沟通、团队协作等场景。 ## 🏗️ 核心架构 ### 1. 整体架构 V-IM采用分层架构设计,主要包括以下几层: ``` ┌─────────────────────────────────────────────────────────┐ │ 客户端层 │ │ Web/Desktop/Mobile 客户端,基于WebSocket协议通信 │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 接入层 │ │ WebSocket服务 (T-IO),处理连接管理、心跳检测、消息转发 │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 业务层 │ │ 消息处理服务、用户管理、好友管理、群组管理等核心业务逻辑 │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 数据层 │ │ MySQL (关系数据)、MongoDB (消息存储)、Redis (缓存、在线状态) │ └─────────────────────────────────────────────────────────┘ ``` ### 2. 核心组件 #### 2.1 WebSocket服务组件 - **TioWebsocketStarter**:WebSocket服务的启动器,配置服务端口、消息处理器等 - **TioWsMsgHandler**:WebSocket消息处理器,处理消息的接收和发送 - **ServerAioListener**:连接监听器,处理连接的建立、关闭等事件 #### 2.2 消息处理组件 - **MessageHandlerService**:消息处理服务接口,定义消息处理的核心方法 - **singleMessageHandlerService**:单机模式下的消息处理器实现 - **MessageLogService**:消息日志服务,记录消息的发送和接收日志 #### 2.3 连接管理组件 - **ConnStatusService**:连接状态服务,管理用户的在线/离线状态 - **ChannelContext**:通道上下文,存储连接相关的信息 #### 2.4 配置组件 - **VimConfig**:系统配置类,管理系统的各种配置参数 - **RedisConfig**:Redis配置类,配置Redis连接和序列化方式 - **MongoConfig**:MongoDB配置类,配置MongoDB连接 ### 3. 技术栈 | 技术类别 | 技术选型 | 版本 | 用途 | |---------|---------|------|------| | 核心框架 | Spring Boot | 3.x | 应用框架 | | WebSocket框架 | T-IO | 3.x | 实时通讯 | | 数据库 | MySQL | 8.x | 关系数据存储 | | 数据库 | MongoDB | 4.x | 消息存储 | | 缓存 | Redis | 6.x | 缓存、在线状态 | | 安全框架 | Sa-Token | 1.x | 认证授权 | | ORM框架 | MyBatis Plus | 3.x | 数据库访问 | | JSON处理 | FastJSON | 2.x | JSON序列化 | ## 💾 存储架构 ### 1. Redis 存储结构与使用 #### 1.1 存储内容 Redis 主要用于存储实时性要求高的临时数据,包括: | 数据类型 | 键名格式 | 数据结构 | 用途 | |---------|---------|---------|------| | 在线聊天消息 | `message-{fromId}-{toId}`(私聊)
`message-{groupId}`(群聊) | 有序集合(Sorted Set) | 存储实时聊天消息,按时间戳排序 | | 离线聊天消息 | `unread-{userId}` | 有序集合(Sorted Set) | 存储用户离线时收到的消息 | | 用户连接状态 | `conn_status:{userId}` | 字符串(String) | 记录用户的在线/离线状态 | | 登录失败次数 | `login_failure_count:{username}` | 字符串(String) | 记录用户登录失败次数 | | 已读消息 | `read:{userId}:{chatId}` | 字符串(String) | 记录用户对特定聊天的已读时间戳 | #### 1.2 交互时机 ##### 实时消息处理 ```java // 发送在线消息时保存到Redis redisTemplate.opsForZSet().add(key, messageStr, message.getTimestamp()); ``` ##### 用户连接状态管理 ```java // 用户登录时更新连接状态 connStatusService.saveOrUpdate(userId, ConnStatusEnum.ONLINE); // 用户退出时更新连接状态 connStatusService.saveOrUpdate(userId, ConnStatusEnum.OFFLINE); ``` ##### 登录失败处理 ```java // 登录失败时增加失败计数 redisTemplate.opsForValue().increment("login_failure_count:" + username); ``` ##### 已读消息处理 ```java // 标记消息已读 redisTemplate.opsForValue().set("read:" + userId + ":" + chatId, String.valueOf(System.currentTimeMillis())); ``` ##### 定时消息清理 ```java // 每天凌晨4点清理Redis中超过100条的消息 @Scheduled(cron = "0 0 4 * * ?") public void clearRedisMessage() { Set keys = redisTemplate.keys("message-*"); for (String key : keys) { Long counted = redisTemplate.opsForZSet().count(key, 0, System.currentTimeMillis()); if (counted != null) { long count = counted - 100; if (count > 0) { redisTemplate.opsForZSet().removeRange(key, 0, count); } } } } ``` ### 2. MongoDB 存储结构与使用 #### 2.1 存储内容 MongoDB 主要用于存储持久化的消息数据,包括: | 数据类型 | 集合名格式 | 分片策略 | 用途 | |---------|---------|---------|------| | 消息日志 | `message-log-{yyyyMMdd}` | 按天分片 | 存储原始消息内容,用于日志审计 | | 聊天消息 | `chat_message_{shardId}` | 按用户ID/群ID取模分片 | 存储结构化的聊天消息,用于历史记录查询 | #### 2.2 数据结构 ##### MessageLog(消息日志) ```java @Data @Builder @Document(collection = "#{@messageLogCollection}") public class MessageLog { /** 原始消息内容 */ private String content; /** 发送者ID */ private String senderId; /** 发送时间 */ private Long sendTime; } ``` ##### Message(聊天消息) ```java @Data public class Message implements Serializable { /** 消息id */ @Id private String id; /** 消息的来源ID(私聊:用户id,群聊:群组id) */ @Field("chat_id") private String chatId; /** 聊天室类型 friend|group */ @Field private String chatType; /** 消息类型 文本|附件|其他 */ @Field("message_type") private String messageType; /** 消息内容 */ @Field("content") private String content; /** 消息的发送者id */ @Field("from_id") private String fromId; /** 服务端时间戳毫秒数 */ @Field("timestamp") private Long timestamp; /** 扩展字段,格式化为json */ @Field("extend") private JSONObject extend; /** 聊天key */ @Field("chat_key") private String chatKey; } ``` #### 2.3 交互时机 ##### 消息日志记录 ```java // 异步记录消息日志到MongoDB @Override public void logMessage(String text, String userid) { logExecutor.execute(() -> { try { String collectionName = StrUtil.format("message-log-{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))); MessageLog messageLog = MessageLog.builder() .content(text) .senderId(userid) .sendTime(System.currentTimeMillis()) .build(); mongoTemplate.save(messageLog, collectionName); } catch (Exception e) { log.error("异步记录消息日志失败", e); } }); } ``` ##### 聊天消息持久化 ```java // 异步保存聊天消息到MongoDB private void saveChatMessageToDatabase(Message message) { messageLogExecutor.execute(() -> { try { String chatKey = ChatUtils.getChatKey(message.getFromId(), message.getChatId(), message.getChatType()); message.setChatKey(chatKey); // 根据聊天key获取分片集合名 String collectionName = ChatUtils.getCollectionName(message.getFromId(), message.getChatId(), message.getChatType()); mongoTemplate.save(message, collectionName); } catch (Exception e) { log.error("保存消息到数据库失败", e); } }); } ``` ### 3. 数据分片策略 #### 3.1 MongoDB 分片策略 ##### 消息日志分片 - 按日期分片,每天创建一个新的集合 `message-log-{yyyyMMdd}` - 优点:便于按日期查询和归档 ##### 聊天消息分片 - 按用户ID/群ID取模分片,分片键为 `chatKey` - 分片计算公式: ```java // 私聊:根据两个用户ID的差值取模 long f = Long.parseLong(fromId); long c = Long.parseLong(chatId); if (f < c) { return StrUtil.format(COLLECTION_TEMPLATE, (c - f) % SPLIT); } else { return StrUtil.format(COLLECTION_TEMPLATE, (f - c) % SPLIT); } // 群聊:根据群ID取模 return StrUtil.format(COLLECTION_TEMPLATE, (Long.parseLong(chatId)) % SPLIT); ``` - 优点:均匀分布数据,提高查询性能 ### 4. 消息处理流程 1. **消息发送**: - 实时保存到Redis有序集合 - 异步保存到MongoDB - 异步记录消息日志到MongoDB 2. **用户登录**: - 更新Redis中的连接状态 - 从Redis获取离线消息 - 发送离线消息给用户 3. **用户退出**: - 更新Redis中的连接状态 4. **消息已读**: - 更新Redis中的已读状态 5. **定时任务**: - 每天凌晨4点清理Redis中超过100条的消息 ### 5. 技术架构特点 1. **Redis作为缓存层**: - 存储实时和临时数据 - 提供快速的消息查询和推送 - 减少数据库压力 2. **MongoDB作为持久化层**: - 存储历史消息和日志 - 支持灵活的数据结构 - 便于水平扩展 ## 🔧 核心功能清单 ### 1. 💬 全链路即时通讯 * **多模式沟通**:单聊、群聊,支持文本、图片、文件、语音、视频、事件消息 * **消息交互**:引用、撤回、转发、多选、收藏、已读回执 * **智能辅助**:历史消息搜索、未读定位、免打扰、@提醒、系统通知联动 * **可靠性保障**:多端同步、离线重放、ACK 机制保障消息必达 ### 2. 👥 用户与组织治理 * **身份管理**:账号/验证码登录注册,个人资料(头像/状态)管理 * **组织架构**:企业组织树(懒加载/拼音搜索),好友分组与管理 * **群组管理**:一键建群、群公告、群设置、成员统计、权限管控 * **安全机制**:多端互斥登录、文件基于签名去重(秒传) ### 3. 🔐 安全与权限 * **认证授权**:基于Sa-Token的认证授权机制 * **数据加密**:消息加密传输,敏感数据加密存储 * **访问控制**:细粒度的权限控制,支持角色和权限管理 ## 📊 核心功能的业务流程和数据流程 ### 1. 消息发送流程 #### 业务流程 1. 客户端通过WebSocket连接发送消息 2. 服务端接收消息并进行验证 3. 服务端将消息存储到MongoDB 4. 服务端根据消息类型和接收者进行消息转发 5. 接收方客户端接收消息并展示 6. 接收方发送已读回执 #### 数据流程 ``` 客户端 → WebSocket服务 → 消息处理器 → MongoDB存储 → 消息转发 → 接收方客户端 → 已读回执 ``` #### 核心代码 ```java // 消息接收处理 override public Object onText(WsRequest wsRequest, String text, ChannelContext channelContext) { try { SendInfo sendInfo = JSON.parseObject(text, SendInfo.class); String code = sendInfo.getCode(); if (SendCodeEnum.MESSAGE.getCode().equals(code)) { messageHandlerService.handleMessage(channelContext, sendInfo); } // ... 其他消息类型处理 } catch (Exception e) { log.error("处理消息失败", e); } return null; } ``` ### 2. 用户连接流程 #### 业务流程 1. 客户端发起WebSocket连接请求 2. 服务端进行握手验证 3. 客户端发送认证信息(token等) 4. 服务端验证认证信息 5. 服务端绑定用户信息到连接上下文 6. 服务端更新用户在线状态 #### 数据流程 ``` 客户端连接请求 → WebSocket握手 → 发送认证信息 → 服务端认证 → 绑定用户信息 → 更新在线状态 ``` #### 核心代码 ```java // 用户信息绑定 private void bindUserInfo(ReadyAuth readyAuth, ChannelContext channelContext) { try { String token = readyAuth.getToken(); String userId = StpUtil.getLoginIdByToken(token).toString(); // 绑定用户信息到通道 Tio.bindUser(channelContext, userId); // 更新在线状态 connStatusService.setConnStatus(userId, true); // ... 其他绑定操作 } catch (Exception e) { log.error("绑定用户失败", e); Tio.close(channelContext, "绑定用户失败"); } } ``` ### 3. 群聊消息流程 #### 业务流程 1. 客户端发送群聊消息 2. 服务端接收并验证消息 3. 服务端将消息存储到MongoDB 4. 服务端获取群组成员列表 5. 服务端向所有在线群成员转发消息 6. 成员客户端接收消息并展示 #### 数据流程 ``` 客户端 → WebSocket服务 → 消息处理器 → MongoDB存储 → 获取群成员 → 消息广播 → 成员客户端 ``` ### 4. 已读回执流程 #### 业务流程 1. 客户端接收消息 2. 客户端发送已读回执 3. 服务端接收已读回执 4. 服务端更新消息已读状态 5. 服务端通知发送方已读状态 #### 数据流程 ``` 接收方客户端 → WebSocket服务 → 已读回执处理器 → 更新消息状态 → 通知发送方 ``` ### 5. 离线消息实现机制 #### 5.1 核心实现原理概述 V-IM系统采用了基于Redis的离线消息存储机制,针对私聊和群聊分别设计了不同的实现策略: - **私聊**:将离线消息直接存储在Redis的专门有序集合中 - **群聊**:不单独存储离线消息,而是通过比较用户最后读取时间和消息时间戳来动态确定离线消息 #### 5.2 离线消息存储机制 ##### 私聊离线消息存储 当接收方用户离线时,私聊消息会被存储在Redis的**有序集合**中: ```java // VimMessageServiceImpl.java:63-74 @Override public void save(Message message, boolean isOnline) throws Exception { String chatId = message.getChatId(); String messageStr = serializeMessage(message); // 先保存消息到 Redis String key = isOnline ? ChatUtils.getChatKey(message.getFromId(), chatId, message.getChatType()) : StrUtil.format(ChatUtils.UNREAD_TEMPLATE, message.getChatId()); redisTemplate.opsForZSet().add(key, messageStr , message.getTimestamp()); // 异步保存到数据库 saveChatMessageToDatabase(message); } ``` **存储结构说明**: - **键名**:`offline-message-{userId}`(使用 `ChatUtils.UNREAD_TEMPLATE` 生成) - **值**:序列化的消息JSON字符串 - **分数**:消息的时间戳(用于按时间排序) - **数据结构**:Redis有序集合(Sorted Set) ##### 群聊离线消息存储 群聊消息无论接收方是否在线,都只存储在一个共享的Redis有序集合中: ```java // ChatUtils.java:24 public static final String GROUP_TEMPLATE = "message-{}"; // VimMessageServiceImpl.java:67 String key = isOnline ? ChatUtils.getChatKey(message.getFromId(), chatId, message.getChatType()) : StrUtil.format(ChatUtils.UNREAD_TEMPLATE, message.getChatId()); ``` **存储结构说明**: - **键名**:`message-{groupId}`(使用 `ChatUtils.GROUP_TEMPLATE` 生成) - **值**:序列化的消息JSON字符串 - **分数**:消息的时间戳 - **数据结构**:Redis有序集合 #### 5.3 离线消息检索机制 ##### 私聊离线消息检索 当用户上线后,系统会调用 `handleOffLineMessage()` 方法加载私聊离线消息: ```java // AbstractMessageHandlerService.java:194-204 @Override public void handleOffLineMessage(ChannelContext channelContext) throws Exception { String userId = channelContext.userid; TioConfig tioConfig = channelContext.tioConfig; // 发送私聊离线消息 sendMessage(tioConfig, vimMessageService.unreadList(userId, null), userId); List groups = vimGroupApiService.getGroups(userId); for (Group group : groups) { // 发送群聊离线消息 sendMessage(tioConfig, vimMessageService.unreadGroupList(userId, group.getId()), userId); } } ``` 私聊离线消息的具体检索逻辑: ```java // VimMessageServiceImpl.java:225-234 @Override public List unreadList(String chatId, String fromId) { String key = StrUtil.format(ChatUtils.UNREAD_TEMPLATE, chatId); Set set = redisTemplate.opsForZSet().range(key, 0, -1); if (set == null) { return new ArrayList<>(); } List messages = set.stream().map(this::toMessage).toList(); return messages.stream().filter(message -> StrUtil.isBlank(fromId) || message.getFromId().equals(fromId)).collect(Collectors.toList()); } ``` ##### 群聊离线消息检索 群聊离线消息通过比较用户最后读取时间和消息时间戳来确定: ```java // VimMessageServiceImpl.java:243-256 @Override public List unreadGroupList(String userId, String chatId) { String key = ChatUtils.getReadKey(userId, chatId); String value = redisTemplate.opsForValue().get(key); long score = -1; if (StrUtil.isNotBlank(value)) { score = Long.parseLong(value); } Set set = redisTemplate.opsForZSet().rangeByScore(StrUtil.format(ChatUtils.GROUP_TEMPLATE, chatId), score, System.currentTimeMillis()); if (set != null) { return set.stream().map(this::toMessage).collect(Collectors.toList()); } return new ArrayList<>(); } ``` **群聊离线消息判断逻辑**: 1. 从Redis获取用户对该群的最后读取时间(`read-{userId}-{groupId}`) 2. 如果没有读取记录,则从时间戳-1开始获取所有消息 3. 否则,获取该时间戳之后的所有群消息,这些即为离线消息 #### 5.4 离线消息清除机制 ##### 私聊离线消息清除 当用户读取消息后,系统会清除对应的离线消息: ```java // VimMessageServiceImpl.java:280-286 @Override public void receipt(String chatId, String fromId, String type, long timestamp) { String key = ChatUtils.getReadKey(fromId, chatId); redisTemplate.opsForValue().set(key, String.valueOf(timestamp)); clearOfflineMessage(chatId, fromId, type); sendReceiptMessage(chatId, fromId, type, timestamp); } ``` ##### 群聊离线消息清除 群聊离线消息不需要专门清除,因为: 1. 群消息存储在共享的有序集合中,不会占用额外存储空间 2. 离线消息是动态计算的,通过更新用户最后读取时间即可实现清除效果 #### 5.5 关键数据结构和键名 ##### Redis键名模板 | 模板常量 | 键名格式 | 用途 | |---------|---------|------| | FRIEND_TEMPLATE | message-{}-{} | 私聊已读消息存储 | | GROUP_TEMPLATE | message-{} | 群聊消息存储 | | READ_TEMPLATE | read-{}-{} | 消息最后读取时间 | | UNREAD_TEMPLATE | offline-message-{} | 私聊离线消息存储 | | UN_ACK_TEMPLATE | un-ack-message-{} | 未收到回执消息 | ##### 核心数据结构 | 数据结构 | 用途 | 说明 | |---------|------|------| | 有序集合(Sorted Set) | 消息存储 | 使用时间戳作为分数,保证消息按时间顺序存储 | | 字符串(String) | 读取时间存储 | 存储用户对特定聊天的最后读取时间戳 | #### 5.6 离线消息处理流程 ##### 私聊离线消息流程 1. **消息发送**:发送方发送消息 2. **状态检查**:检查接收方是否在线 3. **离线存储**:如果离线,将消息存储在 `offline-message-{userId}` 有序集合中 4. **持久化**:异步将消息保存到MongoDB 5. **用户上线**:接收方上线后,从Redis获取离线消息 6. **消息推送**:将离线消息推送给接收方 7. **清除离线**:接收方读取消息后,清除Redis中的离线消息 ##### 群聊离线消息流程 1. **消息发送**:发送方发送群消息 2. **消息存储**:将消息存储在 `message-{groupId}` 有序集合中 3. **持久化**:异步将消息保存到MongoDB 4. **用户上线**:用户上线后获取所有群列表 5. **离线计算**:对每个群,获取用户最后读取时间之后的所有消息 6. **消息推送**:将这些消息作为离线消息推送给用户 7. **更新时间**:推送完成后,更新用户对该群的最后读取时间 ## 🚀 部署方式 ### 1. Docker部署 V-IM提供了Docker部署方式,简化部署流程: ```bash # 进入docker目录 cd v-im-server-2025/docker # 启动服务 docker-compose up -d ``` ### 2. 本地部署 #### 2.1 环境要求 - JDK 17+ - MySQL 8.0+ - MongoDB 4.0+ - Redis 6.0+ #### 2.2 部署步骤 1. 克隆代码仓库 2. 创建数据库并导入SQL脚本 3. 配置application.yml文件 4. 编译项目 5. 启动服务 ## ⚙️ 配置说明 ### 1. 主要配置文件 - `application.yml`:主配置文件,包含服务器、数据库、Redis等配置 - `application-vim.yml`:V-IM相关配置 - `application-sys.yml`:系统相关配置 ### 2. 核心配置项 #### 2.1 服务器配置 ```yaml server: port: 8080 servlet: context-path: / ``` #### 2.2 数据库配置 ```yaml spring: datasource: dynamic: primary: master datasource: master: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/v-im-2025?useUnicode=true&characterEncoding=utf8 username: v-im-2025 password: v-im-2025 data: mongodb: uri: mongodb://localhost:27017/v-im-2025 redis: host: localhost port: 6379 database: 3 ``` #### 2.3 WebSocket配置 ```yaml # WebSocket配置在代码中通过TioWebsocketStarter类进行配置 # 默认端口:8088 ``` ## 6. 分布式服务端架构改造方案 ### 6.1 改造背景与目标 #### 6.1.1 背景 当前 V-IM 系统采用单机架构,所有客户端连接和消息处理都在单个服务节点上完成。随着用户规模和消息量的增长,单机架构面临以下挑战: - 性能瓶颈:单节点处理能力有限,无法支持大规模并发连接 - 可用性风险:单点故障可能导致整个系统不可用 - 扩展性不足:无法通过水平扩展来应对业务增长 #### 6.1.2 目标 - 构建高可用、可扩展的分布式服务端架构 - 支持水平扩展,动态添加服务节点 - 保证跨节点消息的可靠传递 - 保持与现有客户端的兼容性 ### 6.2 改造架构设计 #### 6.2.1 架构概述 采用**主从节点**架构,系统由一个主节点和多个服务节点组成: - **主节点**: 负责全局消息转发和节点管理,基础功能与服务节点一致 - **服务节点**: 处理客户端连接和本地消息,通过 WebSocket 与主节点保持连接 #### 6.2.2 核心通信流程 ``` 客户端A → 服务节点1 → 主节点 → 服务节点2 → 客户端B ``` 1. 客户端连接到任意服务节点 2. 服务节点与主节点建立 Spring Boot 原生 WebSocket 连接(转发通道) 3. 跨节点消息时: - 发送方客户端 → 所属服务节点 - 服务节点 → 主节点(通过转发通道) - 主节点 → 接收方所属服务节点 - 服务节点 → 接收方客户端 ### 6.3 技术选型 - **业务 WebSocket**: t-io(保持现有实现) - **转发 WebSocket**: Spring Boot 原生 WebSocket(新实现) - **节点发现**: Redis 分布式锁 + 心跳机制 - **会话管理**: Redis 存储用户-节点映射关系 ### 6.4 改造功能点计划 #### 6.4.1 第一阶段:基础框架搭建(已完成) 1. **节点配置管理** - 实现节点类型(master/slave)配置 - 节点ID由每个服务节点部署的时候在配置文件中配置 - 配置转发通道URL和端口 2. **节点发现与注册** - 开发节点注册和心跳检测功能 - 主节点监控所有服务节点状态 3. **转发通道实现** - 开发Spring Boot原生WebSocket转发通道 - 实现服务节点与主节点的连接管理 - 开发转发通道的心跳和重连机制 #### 6.4.2 第二阶段:私聊消息参见跨节点私聊消息的md文档 #### 6.4.2 第三阶段:群聊消息参见跨节点私聊消息的md文档 ### 6.5 核心实现 #### 6.5.1 节点配置 ```yaml vim: cluster: node-type: slave # 节点类型:master/slave node-id: node-001 # 节点唯一标识 node-xxx xxx代表当前部署的机构的部门id master-url: ws://master-node:8081/forward # 主节点转发通道地址 websocket: port: 8080 # t-io 业务 WebSocket 端口 forward-port: 8081 # Spring Boot 转发 WebSocket 端口 ``` #### 6.5.2 消息处理扩展 扩展 `MessageHandlerService`,实现分布式消息处理: ```java @Service(value = "distributedMessageHandlerService") public class DistributedMessageHandlerServiceImpl extends AbstractMessageHandlerService { @Override protected void deliverOnlineMessage(ChannelContext channelContext, SendInfo sendInfo, WsResponse wsResponse) throws Exception { Message message = parseMessage(sendInfo); String targetUserId = message.getChatId(); // 检查目标用户是否在本地节点 if (isUserOnLocalNode(targetUserId)) { // 本地节点消息处理 Tio.sendToUser(channelContext.tioConfig, targetUserId, wsResponse); } else { // 跨节点消息处理,发送到主节点转发 forwardToMaster(sendInfo); } vimMessageService.save(message, true); } } ``` #### 6.5.3 用户-节点映射 使用 Redis 存储用户 ID 与节点 ID 的映射关系: - 格式:`user-node:{userId} → {nodeId}` - 节点启动时自动注册,定期发送心跳 - 主节点监控所有节点状态 #### 6.5.4 消息转发协议 ```java public class ForwardMessage { private String messageId; // 消息唯一标识 private String sourceNodeId; // 源节点ID private String targetNodeId; // 目标节点ID private String targetUserId; // 目标用户ID private String targetGroupId; // 目标群组ID(群消息时使用) private String messageType; // 消息类型:单聊/群聊 private String content; // 消息内容(JSON格式) private long timestamp; // 时间戳 } ``` ### 6.6 改造难点分析 #### 6.6.1 技术难点 1. **跨节点消息可靠性** - 如何确保跨节点消息不丢失、不重复 - 实现消息的可靠投递和确认机制 - 处理网络异常和节点故障时的消息补偿 2. **节点一致性维护** - 如何确保用户-节点映射的一致性 - 处理节点上下线时的映射更新 - 实现节点状态的实时同步 3. **性能优化** - 减少跨节点消息的转发延迟 - 优化群聊消息的广播效率 - 避免主节点成为性能瓶颈 4. **故障转移机制** - 如何实现主节点故障时的自动选举 - 处理服务节点故障时的用户迁移 - 确保故障转移过程中消息不丢失 #### 6.6.2 业务难点 1. **兼容性保障** - 确保现有客户端无需修改即可接入分布式架构 - 保持现有API的向后兼容性 - 处理新旧版本节点混合部署的情况 2. **数据迁移策略** - 如何将现有单机数据平滑迁移到分布式架构 - 处理迁移过程中的数据一致性 - 实现无停机数据迁移 3. **运维复杂度** - 如何监控和管理多个节点 - 处理分布式环境下的日志收集和分析 - 实现分布式系统的故障定位和排查 ### 6.7 部署与运维 - **容器化部署**: 使用 Docker 容器化部署主节点与服务节点 - **监控告警**: 节点状态、消息延迟、连接数与消息量统计监控 - **升级维护**: 滚动升级策略、配置热更新、日志收集与分析 --- ### 🖥️ PC 端 (Electron + Web) ![Electron](https://img.shields.io/badge/-Electron-47848F?style=flat-square&logo=electron&logoColor=white) ![Vue3](https://img.shields.io/badge/-Vue3-4FC08D?style=flat-square&logo=vue.js&logoColor=white) ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) ![Vite](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&logo=vite&logoColor=white) * **UI 框架**:Element Plus * **状态管理**:Pinia (持久化) * **平台支持**:Windows, Linux (AMD64/ARM64), macOS (Intel/Silicon), Web, 信创环境 ### 📡 后端服务端 (V-IM Server Pro) ![SpringBoot](https://img.shields.io/badge/-SpringBoot-6DB33F?style=flat-square&logo=springboot&logoColor=white) ![Netty](https://img.shields.io/badge/-TIO-2277DA?style=flat-square&logo=java&logoColor=white) ![Redis](https://img.shields.io/badge/-Redis-DC382D?style=flat-square&logo=redis&logoColor=white) ![MySQL](https://img.shields.io/badge/-MySQL-4479A1?style=flat-square&logo=mysql&logoColor=white) ![MongoDB](https://img.shields.io/badge/-MongoDB-47A248?style=flat-square&logo=mongodb&logoColor=white) * **核心框架**:Spring Boot + T-IO / Netty * **鉴权安全**:Sa-Token * **通讯协议**:WebSocket + 心跳保活 + 自动重连 ---