Giter VIP home page Giter VIP logo

whale-shark's Introduction

WhaleShark

IM-WhaleShark

WhaleShark(鲸鲨)是基于 Netty 实现的高性能分布式 IM 即时通讯系统

介绍 | 技术栈 | 模块职责 | 功能实现 | 架构设计 | 快速开始 | 状态码简述 | 与我联系

介绍

WhaleShark(鲸鲨)是基于 Netty 实现的高性能分布式 IM 即时通讯系统,它支持对接用户自定义多端登录方式,它实现了消息的四大特性(实时、有序、可靠、幂等),架构层面吸收业界大牛的技术文章以及成熟产品实现,特别适合新手小白进行 IM 的学习与参考

技术栈

使用到 Netty、Redis、Redisson、RabbitMQ、Zookeeper、RPC、Feign 等主流技术栈

  • Netty 实现高性能消息收发,应用层握手(用户登录登出),心跳检测(挂后台)
  • Redis 和 Redisson 客户端实现用户 Session 信息的存储、发布订阅模式实现路由层信息缓存
  • RabbitMQ 解耦对接 TCP 网关服务和逻辑层交互、保证分布式下消息顺序性
  • Zookeeper 注册中心及时感知服务节点上线下线情况
  • Feign RPC 方式解耦消息发送方合法性校验

模块职责

采用 DDD 架构**搭建各个模块层级,并使用大量设计模式优化架构,使项目易阅读、可扩展
项目树如下

im-system-whale-shark
├─ im-codec           接入层:负责网关服务配置文件集成、私有协议包结构定义、消息编解码以及需要发送给 TCP 服务的数据包定义
├─ im-common          基础层:负责定义整个 IM 架构所有常量、状态码、错误码、基础数据模型
├─ im-domain          领域层:负责定义用户、好友、群组等多个领域的逻辑,以及消息的发送服务
├─ im-message-store   消息存储层:通过 MQ 将消息异步持久化落库, 很薄的一层
├─ im-infrastructure  基础层:负责定义底层组件如 redis、zk、mq 的配置逻辑,回调机制和基类消息发送
└─ im-tcp             网关层:负责定义心跳机制、监控用户消息读取以及上线下线、Netty 消息通道以及 WebSocket 全双工通道

亮点

  • 设计模式重构
    • 使用策略模式重构用户操作指令逻辑
    • 使用状态模式重构用户自定义多端登录方式
    • 使用模板模式重构消息接收器(群聊、单聊的消息接收器逻辑十分相似)
  • 使用 Redis 缓存用户信息的方式模拟路由层,实现跨服务之间的多 Channel 通讯
  • 使用 Redisson 发布订阅模式,监听用户登录行为,发送用户下线通知。存储用户多端设备的 Session 信息
  • 使用 Rabbitmq 处理分布式消息顺序性, 异步执行历史消息落库持久化等问题, 并且解决线上 MQ 消息积压和消息不一致等问题
  • 使用拦截器机制, 通过 HMAC-SHA256 加密算法实现接口加密防刷, 提升系统安全性
  • 单聊、群聊服务优化改造(实时性、有序性、可靠性、幂等性)
    • 实时性: 使用线程池、MQ 异步持久化、RPC 解耦合法性校验大幅提升消息实时性, 接口响应从 400ms 提升至 15ms
    • 可靠性: 通过应用层两次握手, 即发送方接收上、下行 ACK 确保消息可靠性, 解决消息丢失问题。消息丢包率从 6.32% 下降到 0
    • 有序性: 使用 Redis 原子递增 incr 保证消息有序性, 解决消息乱序问题
    • 幂等性: 通过防重 ID, 服务端、客户端缓存消息等幂等性手段遏制消息重复现象, 并限制消息的无限制重试, 接口异常情况从 8.13% 下降到 0
  • 实现单聊、群聊消息已读和已读回执功能
  • 采用读扩散实现单聊、群聊离线消息拉取

快速开始

数据库环境

导入 whale-shark/assert/sql/im_core.sql 文件

Docker 环境部署

如果是部署到服务端,注意防火墙是否拦截端口

redis:

docker run -d --name redis -p 6379:6379 redis

zookeeper:

docker run -d --name zookeeper -p 2181:2181 zookeeper

rabbitmq:

docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq
  • 其中 15672 端口是连接 web 端页面的, 5672 端口是 Java 后端程序访问 rabbitmq 的

后端启动

后端有三个服务需要开启, 分别为:

  • im-tcp 包下的 Starter 程序 com.bantanger.im.tcp.Starter。它用于构建 TCP 网关服务, WebSocket、Socket 的连接, 消息发送, 回调以及路由等等基层操作。socket 的端口号是 9001, websocket 的端口号是 19001
  • im-domain 包下的 Application 程序 com.bantanger.im.domain.Application。它用于构建业务逻辑服务, 如用户、好友、群组的创建, 更改, 删除, 与数据库、缓存进行逻辑交互。端口号为 8000
  • im-message-store 包下的 Application 程序 com.bantanger.im.message.Application。它用于实现 MQ 异步消息落库存储服务。端口号为 8001

py 脚本测试

whale-shark/im-domain/src/test/python/ 包下所有测试文件都可运行

具体功能可自行研究, 现已用 websocket 全面代替

websocket 测试

whale-shark/im-tcp/src/main/resources/WebSocket.html 暂时较为简陋, 本地测试, 需开启后端三个服务

主要浏览方式通过 F12 查看服务端发送的 json 格式是否正确

如图所示: 平台 [appId = 10001] 的用户 [userId = 10001, clientType = 3, imei = 200] 在登录 Login 之后向群组 [groupId = 27a35ff2f9be4cc9a8d3db1ad3322804] 通过操作指令群发模式[command = 2104] 发送一条群组消息 websocket功能测试

架构设计

私有协议

IM 的私有协议确立信息如下:

+------------------------------------------------------+
| 指令 4byte     | 协议版本号 4byte  | 消息解析类型 4byte  |
+------------------------------------------------------+
| 设备类型 4byte  | 设备号长度 4byte  | 平台ID 4byte      | 
+------------------------------------------------------+
| 数据长度 4byte  | 数据内容(设备号 imei 4byte + 请求体)   |
+------------------------------------------------------+

其中请求头共有:7 * 4 byte = 28 byte

消息投递过程

一条消息的流转 流程如下:

  1. 客户端 userA 发送一条消息到服务器, 消息通过私有协议转化为二进制序列化, 通过 TCP 三次握手保证消息在传输层的稳定性(上下行 ACK 保证消息在应用层的稳定性 )
  2. 服务器将消息投递到 MQ 生产者
    1. 生产者将这条消息推送到 MQ 的指定队列 (queue = MessageServer2Im) 进行处理
    2. 消费者通过订阅消息传输队列接收这条消息
  3. 客户端 userB 成功接收服务端投递过来的消息, 完成整个消息的发送接收

路由层

分布式路由层

由于使用了分布式, 用户的信息会因为负载均衡分布在不同的服务器上,怎么保证多 Channel 的跨节点通讯就显得额外的重要。

在这里我们使用了 Redis 来模拟路由层, k-v 分别用 toUserId 和目标用户所在的服务器节点。当然, 可能会出现同一个 toUserId 对应多个路由层节点,对于此我们只取第一个成功获取即可

消息请求通过 Netty 传递到路由层,在路由层上有高速缓存表,可以快速的获取到消息传递的目的地,并且由于路由层是无状态的,可以很方便进行水平扩容,搭建集群

流程如下:

  1. 发送方将消息发送到自己所在的节点。
  2. 发送方所在的节点进行消息路由,根据接收方所在的节点,将消息路由到对应的节点。
    1. 由于我们在用户的 Session 中设计了 brokerId 和 ip 地址, 因此我们可以获取到对方的节点
    2. 之后的通讯通过点对点 (TCP连接、UDP协议、WebRTC技术) 或是广播 (只广播一个节点) 将消息发送
  3. 接收方在自己所在的节点上接收到消息并处理。

但其实本质上是集中式路由,所有的消息都通过**服务器进行路由,**路由器**服务器根据消息的接收方信息来决定将消息路由到哪个节点。相比分布式路由,集中式路由实现简单,但是**服务器成为了系统的瓶颈,可能会限制系统的性能和可扩展性。

分布式路由:将消息路由的权力分散到各个节点中,每个节点都可以进行消息的路由请求处理。这种方式可以提高系统的可扩展性和容灾性,但实现会比较复杂。(后续真正实现路由层的时候会考虑分布式路由的架构设计)

读写扩散模型

写扩散

  • 在架构中, 单聊会话消息采用写扩散

读扩散

  • 在架构中, 群聊会话消息采用读扩散

消息同步模型

多端消息同步的弊端:

由于 WhaleShark 实现了用户多端同步,因此需要保证一条消息既同步给发送方的其他端,又得保证消息能发送给目标对象的所有端。一条消息的处理流程如下: 多端消息同步的弊端 如架构图所演示,一条消息如图所示就裂变成三条消息了,如果说端的类型更多 (设计上是有六种端: Windows、Mac、Web、Android、IOS、WebApi) 但实际上我们基本是通过 WebApi 来接收消息, 再同步给其他端,也就需要裂变出 5 * 5 - 1 = 24 条消息

一口气发送如此多条消息对于服务器来说,压力是巨大的,因此我们需要重新设计一些策略来实现消息同步

多端消息同步改进:

多端消息同步改进

群聊消息同步流程:

群聊消息同步流程

消息可靠传达模型

我们难以保证消息全都可靠传达,不会产生丢失现象,在 IM 系统中也不允许丢失一条消息。

在设计上,我们采用应用层两次握手(上下行 ACK)来保证消息在应用层的可靠传达 上下行ACK

  • 上行 ACK:服务端发送给消息发送方的接收确认 ACK
  • 下行 ACK:目标用户发送给消息发送方的接收确认 ACK

ACK 应答报文中的数据格式:

CheckACK(String messageId, String messageSequence)

在线用户消息接收

在线用户消息确认

离线用户消息接收

离线用户消息确认

ACK 丢失现象

下面分别探讨上下行 ACK 丢失现象的处理流程

ACK 丢失现象解决策略: 由于我们 ack 中含有 msgId, 可以在客户端维护一个等待 ACK 接收队列,客户端(发送方)发送消息之后,在本地维护当前消息的 msgId,直到上行、下行 ACK 都被接收方确认之后才将这条消息从队列里移除

  • 当上行 ACK 丢失,发送方认为要么是服务端 ACK 由于网络波动晚抵达了,要么是服务端没有接受到自己发送的消息,无论是哪种情况,都会进行消息的超时重投(沿用上一次的 msgId,做防重处理)
  • 当下行 ACK 丢失,发送方只会认为是服务端没有将消息发送过来,到底是服务端没有接受到在线用户的接收确认还是代替离线用户发送接收确认数据包丢失,客户端并不关心,消息超时重投

至于消息如何保证幂等性,不重复消费,可看下文:TODO 消息幂等性

状态码定义

详细请看:com.bantanger.im.common.enums.command

单聊状态码:

消息详细内容 对应状态码
单聊消息发送指令 1103(0x44F)
单聊消息 ACK(服务端接收发送方消息的确认应答,用于发送方消息多端同步) 1046(0x416)
单聊消息收到 ACK(服务端代替离线用户接收消息的确认应答) 1107(0x453)
单聊消息已读确认 1106(0x452)
单聊消息已读通知给同步端 1053(0x41D)
单聊消息已读回执,给原消息发送方 1054(0x41E)

群聊状态码:

消息详细内容 对应状态码
群聊消息发送指令 2104(0x838)
群聊消息 ACK(服务端接收发送方消息的确认应答,用于发送方消息多端同步) 2047(0x7ff)
群聊消息已读确认 2106(0x83a)
群聊消息已读通知给同步端 2053(0x805)
群聊消息已读回执,给原消息发送方 2054(0x806)

前后端对接

IM 服务采用 SDK 方式集成到前端代码。一个大致的流程演示如下: SDK执行流程 对此我已经大致实现了后端的一个 im-app-server 与前端的 SDK 进行对接,欢迎前端同学与我一起来完善 WhaleShark

联系

如果有什么不懂的设计, 可通过提 ISSUE 或者是加我微信进行探讨

欢迎与我联系交流,微信二维码为(注明来意喔~):

与我联系.png

whale-shark's People

Contributors

bantanger avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.