# echo-im
**Repository Path**: yuecoding/echo-im
## Basic Information
- **Project Name**: echo-im
- **Description**: 一个IM(即时通讯)项目的后端系统。支持IM系统核心功能:好友、私聊、群聊、离线消息、发送语音、图片、文件、emoji表情、回执消息、视频聊天等
严格遵循IM系统的四大原则:实时性、幂等性、不丢失、时序性。
主要基于 Spring Boot 和 Netty 开发
- **Primary Language**: Java
- **License**: MIT
- **Default Branch**: master
- **Homepage**: https://gitee.com/brother-one/echo-im
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 32
- **Created**: 2024-08-26
- **Last Updated**: 2024-08-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
Echo IM 1.0.0
基于 SpringBoot 和 Netty 开发的即时通讯(IM)系统
# 📊介绍
**项目简介** :一个 IM(即时通讯)项目的后端系统。
**支持 IM 系统核心功能** :好友、私聊、群聊、离线消息、发送语音、图片、文件、emoji 表情、回执消息、视频聊天等
**严格遵循 IM 系统的四大原则:实时性、幂等性、不丢失、时序性。 、**
**主要技术栈** :SpringBoot、MyBatis、SpringSecurity、Netty、MySQL、Redis、RabbitMQ、WebSocket、Sa-Token、 Redission、Minio
**详细文档** :https://gitee.com/brother-one/echo-im/tree/master/doc/md
# 🔌系统架构(详见本项目 doc 文件夹)
## 🔌协议设计
```
+----------------+----------------+----------------+----------------+----------------+----------------+
| Magic Number | Version | Serialize Alg. | Command | Data Length | Data |
+----------------+----------------+----------------+----------------+----------------+----------------+
| 4 Bytes | 1 Byte | 1 Byte | 1 Byte | 4 Bytes | Variable Bytes |
+----------------+----------------+----------------+----------------+----------------+----------------+
```
**Magic Number** (魔数):用于标识该数据包是合法的自定义协议数据包,避免错误地解析其他类型的数据。你在代码中使用了 0x12345678 作为魔数。
**Version** (版本号):用于标识协议的版本号,方便在未来进行协议升级时做兼容处理。
**Serialize Algorithm** (序列化算法):用于标识数据的序列化和反序列化算法。例如,你可能会有 JSON、Protobuf 等不同的序列化方式。
## 🗄️数据库
| 表名 | 中文 | 创建时机 | 备注 |
|--------------------|-------|------------------|--------------|
| im_user | 用户表 | 用户注册 | 记录用户信息 |
| im_friend | 好友表 | 添加好友 | 双向记录好友关系 |
| im_private_message | 私聊消息表 | 发送私聊消息 | 记录与好友之间的聊天消息 |
| im_group | 群组表 | 创建群组 | 记录群组中的成员信息 |
| im_group_member | 群组成员表 | 邀请好友进入群聊 | 记录群聊中的成员信息 |
| im_group_message | 群聊消息表 | 发送群聊消息 | 记录群聊中的所有消息 |
| im_sensitive_word | 暂未实现 | 暂未实现 | 暂未实现 |
| im_offline_message | 离线消息表 | 用户离线时接收到消息 | 只存放私聊消息 |
| task | 任务表 | 当 RabbitMQ 任务失败的时候 | 用于任务补偿 |
**说明:**
- **im_group_member** :此表通过 last_readed_message_id 字段来实现当前用户的消息游标,语义为用户已经成功读取此游标之前的所有消息
- **im_friend**:此表存储了双向的好友关系,便于维护双方的好友备注等信息
- **im_offline_message**:此表与 redis zset 共同维护用户的离线消息,权衡离线消息的高可靠和高并发
- **task**:用于任务补偿机制,任务失败-> 任务入库 -> 定时重发/记录备份
## 🏗️模块架构
| 模块名 | 中文 | 说明 | |
|--------------------------|----------|----------------------------|---|
| im-common | 通用模块 | | |
| im-repository | 仓储模块 | | |
| im-infrastructure | 基础设施模块 | | |
| im-domain | 领域模块 | | |
| im-client-starter | 消息委托 SDK | engin 委托此 sdk 转发消息到其他 engin | |
| im-engin-server | 消息引擎服务 | 与用户进行即时通讯 | |
| im-platform-server | 平台业务服务 | 处理耗时业务 | |
| im-storage-server | 数据存储服务 | 存储全量消息、存储离线消息 | |
| im-offline-notify-server | 离线消息通知服务 | 当用户离线时收到消息,可通过第三方(邮件等)通知用户 | |
## 📊最新系统架构图
用户的收发实时消息都是直接和 Websocket 进行交互,摈弃了旧版中的“通过 HTTP 协议发送消息”。但是事务级业务,例如创建群聊,好友关系建立等须通过平台业务系统进行处理,平台业务系统也可以委托 im-client-starter 发送实时消息。例如实时好友申请通知,群聊邀请等。

## 🛡️可靠性保证


## 📜旧版系统架构图
用户通过 **HTTP** 请求将消息发送到 **im-platform** ,平台负责进行权限校验、数据脱敏和异步存储等工作,并为消息添加元数据,根据这些元数据, **im-client** 执行相应处理,并将消息发布到 **RabbitMQ** 。消息按照路由规则到达 **im-server** ,最终通过 **WebSocket** 协议实时推送给前端接收用户。
.png)
# ✒️简历上的亮点
1. **调研与选型** :深入调研了多种 IM 系统架构设计和实现方式,明确技术选型,确保项目技术方案的可行性和高扩展性
2. 基于 Sa-Token+Redis 构建 **认证模块** :实现 **双 Token** 授权, **强制下线** ,同终端互斥登录,集成 Gitee、Github 第三方登录
3. **高可靠的 WebSocket 信道** :实现 连接鉴权、 **心跳检测与续期** 、断线重连机制,并实时上报用户状态,保证信道可靠性和持续性
4. **可靠性** :实现应用层 ACK **时间轮** 队列、离线消息的 **双重存储** + **会话级分布式锁** 、MQ **任务补偿** 、可靠信道,保证高可靠
5. **实时性** :通过双级缓存、高效的 ACK 和离线消息机制、RabbitMQ 异步解耦、Netty NIO 及分布式部署,保证实时性
6. **扩展性** :设计可扩展的底层数据模型, **防止消息扩散风暴** ,确保 **功能内部复用** ,灵活运用设计模式,保障扩展性
7. 去中心化的 **消息推送架构**:基于 RabbitMQ 构建去中心化消息推送架构,使系统具备集群化部署能力,提高并发处理性能
8. 重构与封装:将消息分发、消息队列(MQ)和仓储功能封装为 Spring Boot **Starter** , **遵循 SPI 规范** ,实现低耦合与高扩展性
9. 使用 **Nginx** 部署项目:实现 反向代理、动静分离,并配置负载均衡和 HTTPS,确保系统的高可用性和安全性
# 💠核心特性
## 💬私聊
### 不丢失
**不丢失的定义:** 发送方点击发送按钮之后消息必达
**消息已达的定义:** 发送方收到此消息的应答包(ack)
如果发送发送方没有收到 ack,则进行一定的重试策略,如果收到 ack 了,就说明消息一定被接收方所接收
接收方有两种状态:在线状态、离线状态
* 当接收方不在线的时候,服务器代理接收方发送伪 ack 响应发送方,并将此离线消息进行持久化,待接收方上线之后拉取离线消息
* 当接收方在线时,由接收方收到消息之后对发送方进行 ack 回复
上述两种过程,只要发送方收到了任意一种 ack,就证明消息已达
如果发送方没有收到 ack,发送方需进行重试,重试策略待商榷
#### 工程实现
发送方发送消息时,不用上传 msgId,msgId 由服务器统一生成,
此消息的结构为【 seq recvId content 】,
seq 的组成为 【userId+当前连接唯一标识+连续递增值】
* userId 可以不要,因为每条消息务必有 userId
* 当前连接唯一标识:当每次用户连接成功之后,netty 服务器返回一个当前连接的全局唯一标识
**消息的流转过程:**
1. 【seq recvId content】**seq 由客户端生成** 到达 netty 服务器,服务器为此消息生成全局唯一 msgId,转发给接收方
2. 如果接收方离线,代理回复伪 ack【ack = seq,msgId = xxx】;如果在线,则转发给接收方
3. 接收方收到消息之后,回复 ack【ack = seq,msgId = xxx】,证明此消息已收到
4. 发送方收到此 ack 之后,认为消息已达,将该发送任务从缓存中删除
5. 如果没有收到 ack,则进行一定的重试,重试的时候务必保证 seq 的幂等性,seq 的幂等性等于此消息的幂等性
**seq 的作用**
1. seq 和 ack 配合完成一应一答机制,消息不丢失
2. seq 是绝对递增的,保证了消息的顺序性
3. seq 协助保证消息幂等性,因为要保证消息幂等性,务必保证消息的唯一标识要在客户端生成,不然就是空谈幂等性,但是又因为此唯一标识在客户端生成,并不能作为 msgId 来保证全局顺序,所以采用 seq+msgId 来共同保证幂等性+顺序性
**私聊消息回复 ack 的埋点**
1. im-client-starter 判断接收方离线时
2. netty 推送消息时判断本地不存在该接收方的 channel 时
3. 接收方成功接收到消息时
### 幂等性
首先幂等性的保证是根据 msgId 来进行保证,其次接收方在展示消息的时候,根据 seq 对消息进行去重判断,如果 seq 相同,进一步比对消息内容,如果消息内容相同,则证明此消息重复
#### 为什么 msgId 不能证明幂等性?
因为幂等性的含义是一个消息的唯一标识,发送方发送了一次消息,此消息在第一次发送以及整个重试阶段都会具有唯一的 seq,此 seq 才能证明消息的唯一性
因为我们的 msgId 实在服务端生成的,如果没有此 seq,那么发送方发送的消息只有【recvId content】,这种情况我们无法判断是用户主观的重复发送消息还是重试机制进行的重复发送消息
所以客户端每点击一次发送按钮,都会生成一个当前连接唯一的并且连续递增的 seq,
### 时序性
当接受方接收到消息时,不同连接(连接唯一标识)则根据 msgId 进行排序,msgId 是服务器生成的具有时序性的 Id,连接标识相同的 msg 则根据 seq 进行排序,既能保证全局顺序,也能保证局部顺序(同一个发送方的本地发送顺序)
如果是不同连接,不同的 websocket 连接,那么只能由服务器生成的 msgId 来审判它们的先后顺序,如果是相同的 websocket,则由 seq 来审判它们的先后顺序
### 实时性
### 离线消息的不丢失
离线消息以 msgId 的形式存入 redis list,如果超出指定数量,则进一步存放到 redis
## 👥群聊
### 不丢失
发送方发送消息的时候,只需要保证将消息成功发送到服务器就可以了,由服务器去转发消息,如果接收方没有收到,根据 id 连号机制去服务器获取丢失的消息
群聊的消息为连号的消息,每个群的所有消息都是严格递增的,可通过 redis 的 incr 来实现,然后群组员表里面有此成员已读最大消息的 id【last_readed_msg_id】可以假设此字段是一个游标,用户根据此游标以及群消息 id 严格递增的特性,来保证消息不丢失,但是无法避免连号丢失的问题
我们在群聊表中设置了 order_id 来保证群聊消息的连号性,严格递增
发送方发送消息的时候务必携带 seq,服务端收到消息的时候回此 seq 对应的 ack
所以群聊不丢失 = last_readed_msg_id+order_id
群聊消息 ack 的时候要携带【messageId order_id ackId】
**群聊消息 ack 埋点**
1. 当服务器成功收到发送方的群聊消息的时候
### 幂等性
发送方发送消息的时候的参数为【groupId content】
所以就有一个很棘手的问题,如何保证消息的幂等性,我们在私聊中通过 seq 来保证消息的幂等性,群聊中同样也可以用这个思路,
### 时序性
群聊消息的时序性和私聊消息可采用相同的思想,分为不同连接和相同连接
如果接收到的消息为同一个用户的消息 根据 seq 进行判断先后顺序,反之,则通过 msgId 进行判断顺序
## 📵离线消息
### 私聊
私聊中的离线消息全部存储到 redis 的 list 中,只存 messageId,也可以存全量消息无所谓,因为我们的每条消息入库之后就不会被更改了,撤回已读这些功能不会修改原有的消息
如果 redis 满了,有一张专门的离线消息表,也可以存储离线消息
#### 离线消息的拉取
如何拉取离线消息才能保证离线消息可靠拉取?
私聊中离线消息的存储模式是 离线消息存入 redis 的 list,保证先来后道,这个原则不能打破,然后比如超出阈值(例如 1k),我们就从 list 中取出 500 条来存入 mysql,怎么取呢,从左往右取,保证先来的消息先进入 mysql,
然后拉取离线消息的时候从 list 开始拉取,我们假设新到的消息全部都在右边,我们先指定一个索引,例如一次拉取 200 条,这样我们现在 redis 中遍历 0-200 条数据传回前端,前端接收成功之后接着请求 201-400 的消息,这时我们就可以删除 0-200 条消息,也就是一次拉取也等于一次确认,既保证可靠性,也节省了网络开销
有这样一个假设 拉取离线消息的时候上传参数 lastId size,当第一次拉取的时候 lastId = 0 size = 200
如果 lastId = 0,说明是首次拉取,这时候首先要进行会话级分布式锁,保证不会有多端同时拉取离线消息造成混乱,然后 lastId = 0 也说明是首次拉取,如果是首次拉取就不用删除,就只是取数据然后返回给前端
然后第二次 lastId = 上一次拉取的最后一个下标+1,在我们的例子中就是 201
这时候后端就先删除 0-200 的消息,然后返回给前端 200~200+size
就这样一删一发 直到返回给前端的消息为 0,
如果返回给前端的消息数量为 0,就说明拉取离线消息完毕,当拉取完毕的时候,要释放会话级分布式锁
**什么是会话级分布式锁?**
这个说来话长,简单来说就是保证 一个用户在同一时间段只能有一个终端设备进行离线消息拉取
当 lastId 为 0 的时候,判断当前分布式锁是否被占用,如果被占用,则直接返回前端“请等待”,如果没有被占用,返回前端离线消息+uuid,并且加上分布式锁,并且在 redis 上面存储此用户目前正在进行离线拉取的会话的 uuid
当 lastId 不为 0 的时候,也就是后续的拉取务必就要带上此 uuid,证明你的身份,这样就可以继续进行离线拉取
当离线消息拉取的消息量为 0 时,释放分布式锁,并删除当前用户正在进行离线拉取的会话的 uuid
至此 离线消息拉取成功
**这么做的意义是什么?**
如果我们在进行离线消息拉取的时候有另外一个终端也要来拉取,
这个时候如果它的 lastId = 0,先要判断有没有分布式锁,有的话直接给他驳回,
如果它的 lastid!= 0,要判断它的 uuid 是不是当前正在操作的会话的 uuid,如果不是也直接给他驳回
**注意** 当 lastId!= 0 的时候不判断分布式锁,只需要判断 uuid
**那么如何协调 redis 和 mysql 中的离线消息拉取?**
我们先拉取 mysql,因为 mysql 的都是旧消息,。。。。太累了,如果有人有想法可以合作,共同商量方案,我真的有点累了
### 群聊
群聊的离线消息拉取是根据 last_readed_order_id,还记得之前说过的每个群聊都有一个 order_id 嘛,我们通过这个比对这两个值已经加上连号机制来判断哪些消息是离线消息,然后进行拉取
## 🗂️分库分表
我们先看看我们有哪些 where 操作,根据 where 来进行分库分表
私聊
* 查询聊天记录 `where ((recv_id=xxx and send_id=xxx) or (send_id=xxx and recv_id=xxx)) and send_time>=xxx `
* 拉取离线消息 `where message_id=xxx`
群聊
* 查询聊天记录 `where group_id=xxx and send_time>=xxx`
* 拉取离线消息 `where group_id=xxx and order_id>=xxx`
### 优化
私聊应该用 recv_id 和 send_id 联合来做分库分表,所以可以设置一个会话 Id 的概念,存储私聊消息的时候,chat_id = Math.max(recv_id , send_id)+Math.min(recv_id , send_id),这样来作为一个 chat_id,存储消息的时候利用此字段来作为索引,然后分库分表也是利用此字段,方便 where 快速查询
群聊肯定是直接根据 group_id 进行分库分表了
然后还没有解决的就是私聊中的拉取离线消息,有两种解决方案,第一种是记录离线消息的时候记录会话 Id,第二种是在离线消息存储的时候存储全量的消息,使离线消息不用再去回查全量数据
## 🔄再论可靠性
### 对“可靠的定义”
- 不丢失:发送方 **成功** 发送消息之后,此消息一定能够到达接收方
- 幂等性:发送方发出的消息保证不重复出现在接收方
- 时序性:发送方在本地发送了 A B C,接收方收到的一定是 A B C,注意是发送方本地的顺序,是发送方决定的而不是服务器决定的
### 不丢失
#### 方案选型
- 我们接下来的讨论室 p2p 的不丢失,不涉及群聊
不丢失的定义是:发送方 **成功** 发送消息之后,此消息一定能够到达接收方,关键在于成功。用户消息发送成功的定义就是用户收到了一个 ack
不然用户网络出问题,消息并没有成功发送到服务器,用户只是简单的点了一个按钮,这当然不能叫做“成功”
我们目前的方案有两种:
- 方案一:当发送方消息发送到服务器了,服务器直接 ack,并且保证此消息一定能够到达接收方;
- 方案二:当发送放发送消息之后,如果接受方离线,服务器代为 ack;如果接收方在线,由接收方 ack;
方案一会增大服务器的压力,服务器必须要保证此消息可靠到达接收方,服务器务和接收方进行消息确认,要进行 ack,无疑增大了服务器的压力
方案二会面临大量的丢包问题,虽然在方案二中服务器是“无状态”的,就是说服务器不保证消息的最终一致性
可以简单理解为每个消息都有三种状态
1. 消息未达:发送方点击发送按钮但没有收到任何 ack
2. 消息必达:发送方点击按钮之后收到了服务器的 ack
3. 消息已达:发送方点击按钮之后收到了服务器的伪用户 ack 或者接收方的 ack
方案一的好处就是当服务器接收到消息的时候发送方可以迅速确认此消息是可靠的,坏处就是这个可靠性需要服务器去承担
方案二的好处就是当接收方在线的时候服务器不用做消息可靠保证,只有当接收方离线了,服务器才代理发送伪用户 ack;当接收方在线的时候,只是接收方 ack;
整体思路就是,发送方必须要收到 ack 之后此消息才是可靠的,如果没收到,不可能是可靠的,因为可能存在发送方的消息没有到达服务器,这时候何谈可靠?
方案二可以说是 通过增大服务器的压力来保证可靠性,方案二 就是通过延长消息已达的时间来保证可靠性,方案二没有消息必达的概念,所以方案一新增了一个消息必达概念来提前进行消息可靠的确认;
#### 技术实现
接下来我们开始论述方案二如何实现可靠性
方案二有一个很核心的点,就是 ack 会很慢,慢到要接收方收到消息之后发送 ack,然后发送方收到此 ack 之后才认为消息可靠
所以就会面临丢包的问题,假设上述整个过程是 0.5s,那用户并不能在这 0.5s 内进行重发,但是如果在这 0.5s 内丢包,ack 丢包或者发送方发送的消息丢包,用户的体验感就会大大降低,“为什么我的消息发送出去了,等了足足一秒你才说发送失败,早干嘛去了???”
但是我们想保证不增加系统复杂度的前提下用方案二实现可靠,怎么实现?
方案如下。。。。
- 可靠信道---心跳检测:简单来说就是通过客户端发送心跳包,然后服务器回包来保证我们当前的 websocket 链路是可靠的
- 可靠性套---离线判断:简单来说就是当接收方离线的时候我们系统迅速做出判断,将此消息进行离线存储,并且给用户伪 ack
- 可靠信道---断线重连:当前端掉线的时候,前端进行重连机制。。
什么时候消息不可靠(不知道消息是否到达): 反正只要发送方没有收到 ack 就是薛定谔的消息
1. 发送方发送消息到服务器
2. 服务器转发消息到接收方
3. 接收方发送 ack 到服务器
4. 服务器转发 ack 到接收方
方案一中只有过程 1 是不可靠的;方案二中过程 123 都是不可靠的
所以方案二就是 延迟满足+可靠信道 来保证可靠,丢包率 1%(此处丢包率是指首次丢包率),重传丢包率几乎为 0%
所以最主要的就是 echo-im 通过可靠信道保证了延迟满足 99.9%都能满足,就算不能满足,我们也会告诉客户不能满足!哈哈哈
### 时序性
**保证发送方发送顺序与接收方展现顺序一致**
时序性不是简单的通过服务器生成消息 sendTime 然后转发消息,然后此 sendtime 就是消息的时序,也不能用客户端的 sendTime,毕竟客户端的时间准不准谁知道呢
简单的方式就是通过每个会话都有一个 seq,客户端每次上线的时候都每个会话都会拉取一个 seq,seq 起始址为 1,保证绝对递增,如果断线重连,则重新 seq
每次会话中,seq 的递增顺序,就代表发送方的发送顺序,因为务必要保证发送方语义的时序性
# 🛠️功能支持
## 多端登录
: white_check_mark: 同用户同终端登录互斥
## 消息类型
:white_check_mark: **文本消息**:消息内容是普通文本
:white_check_mark: **图片消息**:消息内容为图片 URL 地址、尺寸、图片大小等信息
:white_check_mark: **表情消息**:表情消息为开发者自定义
:white_check_mark: **语音消息**:语音数据需要提供时长
:white_check_mark: **文件消息**:消息内容为文件的 URL 地址、大小、格式等信息,格式不限,最大支持 100M
:white_check_mark: **短视频消息**:消息内容为视频文件的 URL 地址、时长、大小、格式等信息,最大支持 100M
:white_check_mark: **系统通知消息**:包含系统通知消息(例如好友申请通知、群聊邀请通知等)
:white_check_mark: **群 Tips 消息**:系统性通知消息,例如有成员进出群组,群的描述信息被修改,群成员的资料发生变化等
:white_large_square: **地理位置消息**:消息内容为地理位置标题、经度、纬度信息
## 消息功能
:white_check_mark: **离线消息**:用户离线后,将用户未读取的消息存储到离线仓库,待用户上线时自动拉取
:white_check_mark: **多端同步**:多终端在线时消息同步,可同时收到消息
:white_check_mark: **历史消息**:支持本地历史消息和云端历史消息
:white_check_mark: **消息撤回**:撤回投递成功的消息,默认撤回 2 分钟内的消息。撤回操作仅支持单聊和群聊消息
:white_check_mark: **消息已读**:告知发送方,此消息已读
:white_check_mark: **已读回执**:查看点对点会话中对方的已读未读状态
:white_check_mark: **@功能**:群内 @ 消息与普通消息没有本质区别,仅是在被 @ 的人在收到消息时,需要在 UI 上做特殊处理正在输入
:white_large_square: **漫游消息**:在新设备登录时,将服务器记录(云端)的历史消息存储进行同步,默认保存 7 天
:white_large_square: **正在输入**:可以通过在线消息实现
:white_check_mark: **离线推送**:用户不在线时收到消息通过邮箱进行通知
:white_large_square: **消息删除**:使用消息的 remove 方法可以在本地删除消息
## 资料功能
:white_check_mark: **设置用户资料**:用户设置自己的昵称、验证方式、头像、性别、年龄、签名、位置等资料
:white_check_mark: **获取用户资料**:用户查看自己、好友及陌生人资料
:white_check_mark: **按字段获取用户资料**:按照特定字段获取用户资料
## 关系链功能
:white_check_mark: **查找好友**:可通过用户账号 ID 查找好友
:white_check_mark: **申请添加好友**:要选择默认是否需要申请理由,目前是默认不需要
:white_check_mark: **添加好友**:发送添加好友请求
:white_check_mark: **更新好友**:更新同一用户的好友的关系链数据
:white_check_mark: **删除好友**:成为好友后可以删除好友(单向删除)
:white_check_mark: **获取所有好友**:获取所有好友,默认只拉取基本资料
:white_check_mark: **同意/拒绝好友**:收到请求加好友请求的系统通知后,可以通过或者拒绝
:white_check_mark: **好友备注**:成为好友后可以给好友备注
:white_check_mark: **好友状态**:实时更新好友在线设备状态
:white_large_square: **添加到黑名单**:把任意用户拉黑,如果此前是好友关系会解除好友关系
:white_large_square: **移出黑名单**:把用户从黑名单中移除
:white_large_square: **获取黑名单列表**:拉取用户黑名单列表
:white_large_square: **创建好友分组**:创建分组时,可以同时指定添加的用户,同一用户可以添加到多个分组
:white_large_square: **删除好友分组**:删除好友分组
:white_large_square: **添加好友到好友分组**:将好友添加到好友分组
:white_large_square: **将好友从分组中删除**:将好友从好友分组中删除
:white_large_square: **重命名好友分组**:重命名好友分组
:white_large_square: **获取指定好友分组信息**:获取指定好友分组信息
:white_large_square: **好友资料变更通知**:好友资料变更可以收到系统通知
## 群组功能
:white_check_mark: **创建群聊**
:white_check_mark: **邀请入群**
:white_check_mark: **同意/拒绝邀请**
:white_check_mark: **群资料修改**
:white_check_mark: **成员列表**
:white_check_mark: **解散群聊**
:white_check_mark: **移出成员**
:white_check_mark: **历史消息存储**
:white_check_mark: **群成员在线状态实时更新**
## (敬请期待)IM 控制台
## (敬请期待)数据统计
## (敬请期待)实时监控
## (敬请期待)云端审核
# 📦安装教程
1. 执行 doc 文件夹下的 sql
2. 配置 application.yml
3. 启动 Redis
4. 启动 RabbitMQ
5. 启动项目 minio 目录下的 minio.bat
4. 启动 im-server(消息推送系统)
5. 启动 im-platform(平台系统)
6. 启动 im-storage(仓储系统)
# 🏗️ 参与贡献
## 🎋 分支说明
echo-im 源码分为两个分支,说明如下
| 分支 | 作用 |
|---------|-----------------------------|
| master | 主分支,release 版本使用的分支,不接收 pr 或修改 |
| develop | 开发分支,接收 pr 或 pr |
## 🐞 提供 bug 反馈或建议
提交问题反馈请说明正在使用的 JDK 版本
- [Gitee Issue](https://gitee.com/DoubleW2w/echo-im/issues)
## 🪶 贡献代码的步骤
1. 在 Gitee 上 fork 项目到自己的 repo
2. 把 fork 过去的项目也就是你的项目 clone 到你的本地
3. 修改代码(记得一定要修改 develop 分支)
4. commit 后 push 到自己的库(develop 分支)
5. 登录 Gitee ,在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。
6. 等待维护者合并
## 📖 详细文档地址
**详细文档** :https://gitee.com/brother-one/echo-im/tree/master/doc/md
## 📞交流合作
