# 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)
   
* **UI 框架**:Element Plus
* **状态管理**:Pinia (持久化)
* **平台支持**:Windows, Linux (AMD64/ARM64), macOS (Intel/Silicon), Web, 信创环境
### 📡 后端服务端 (V-IM Server Pro)
    
* **核心框架**:Spring Boot + T-IO / Netty
* **鉴权安全**:Sa-Token
* **通讯协议**:WebSocket + 心跳保活 + 自动重连
---