为本身搭建一个分布式 IM(即时通信) 系统

CIM(CROSS-IM) 一款面向开发者的 IM(即时通信)系统;同时提供了一些组件帮助开发者构建一款属于本身可水平扩展的 IM 。面试

借助 CIM 你能够实现如下需求:算法

IM 即时通信系统。缓存

适用于 APP 的消息推送中间件。服务器

IOT 海量链接场景中的消息透传中间件。架构

架构设计负载均衡

下面来看看具体的架构设计。工具

CIM 中的各个组件均采用 SpringBoot 构建。性能

采用 Netty + Google Protocol Buffer 构建底层通讯。学习

Redis 存放各个客户端的路由信息、帐号信息、在线状态等。测试

Zookeeper 用于 IM-server 服务的注册与发现。

总体主要由如下模块组成:

cim-server

IM 服务端;用于接收 client 链接、消息透传、消息推送等功能。

支持集群部署。

cim-forward-route

消息路由服务器;用于处理消息路由、消息转发、用户登陆、用户下线以及一些运营工具(获取在线用户数等)。

cim-client

IM 客户端;给用户使用的消息终端,一个命令便可启动并向其余人发起通信(群聊、私聊);同时内置了一些经常使用命令方便使用。

流程图

总体的流程也比较简单,流程图以下:

客户端向 route 发起登陆。

登陆成功从 Zookeeper 中选择可用 IM-server 返回给客户端,并保存登陆、路由信息到 Redis。

客户端向 IM-server 发起长链接,成功后保持心跳。

客户端下线时经过 route 清除状态信息。

因此当咱们本身部署时须要如下步骤:

搭建基础中间件 Redis、Zookeeper。

部署 cim-server,这是真正的 IM 服务器,为了知足性能需求因此支持水平扩展,只须要注册到同一个 Zookeeper 便可。

部署 cim-forward-route,这是路由服务器,全部的消息都须要通过它。因为它是无状态的,因此也能够利用 Nginx 代理提升可用性。

cim-client 真正面向用户的客户端;启动以后会自动链接 IM 服务器即可以在控制台收发消息了。

详细设计

接下来重点看看具体的实现,好比群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。

IM 服务端

先来看看服务端;主要是实现客户端上下线、消息下发等功能。

首先是服务启动:


因为是在 SpringBoot 中搭建的,因此在应用启动时须要启动 Netty 服务。

从 pipline 中能够看出使用了 Protobuf 的编解码(具体报文在客户端中分析)。

注册发现

须要知足 IM 服务端的水平扩展需求,因此 cim-server 是须要将自身数据发布到注册中心的。

因此在应用启动成功后须要将自身数据注册到 Zookeeper 中。


最主要的目的就是将当前应用的 ip + cim-server-port+ http-port 注册上去。

上图是我在演示环境中注册的两个 cim-server 实例(因为在一台服务器,因此只是端口不一样)。

这样在客户端(监听这个 Zookeeper 节点)就能实时的知道目前可用的服务信息。

登陆

当客户端请求 cim-forward-route 中的登陆接口(详见下文)作完业务验证(就至关于平常登陆其余网站同样)以后,客户端会向服务端发起一个长链接,如以前的流程所示:

这时客户端会发送一个特殊报文,代表当前是登陆信息。

服务端收到后就须要将该客户端的 userID 和当前 Channel 通道关系保存起来。


同时也缓存了用户的信息,也就是 userID 和 用户名。

离线

当客户端断线后也须要将刚才缓存的信息清除掉。

同时也须要调用 route 接口清除相关信息(具体接口看下文)。

IM 路由

从架构图中能够看出,路由层是很是重要的一环;它提供了一系列的 HTTP 服务承接了客户端和服务端。

目前主要是如下几个接口。

注册接口


因为每个客户端都是须要登陆才能使用的,因此第一步天然是注册。

这里就设计的比较简单,直接利用 Redis 来存储用户信息;用户信息也只有 ID 和 userName 而已。

只是为了方便查询在 Redis 中的 KV 又反过来存储了一份 VK,这样 ID 和 userName 都必须惟一。

登陆接口

这里的登陆和 cim-server 中的登陆不同,具备业务性质,

登陆成功以后须要判断是不是重复登陆(一个用户只能运行一个客户端)。

登陆成功后须要从 Zookeeper 中获取服务列表(cim-server)并根据某种算法选择一台服务返回给客户端。

登陆成功以后还须要保存路由信息,也就是当前用户分配的服务实例保存到 Redis 中。

为了实现只能一个用户登陆,使用了 Redis 中的 set 来保存登陆信息;利用 userID 做为 key ,重复的登陆就会写入失败。


相似于 Java 中的 HashSet,只能去重保存。

获取一台可用的路由实例也比较简单:

先从 Zookeeper 获取全部的服务实例作一个内部缓存。

轮询选择一台服务器(目前只有这一种算法,后续会新增)。

固然要获取 Zookeeper 中的服务实例前天然是须要监听 cim-server 以前注册上去的那个节点。

具体代码以下:



也是在应用启动以后监听 Zookeeper 中的路由节点,一旦发生变化就会更新内部缓存。

这里使用的是 Guava 的 cache,它基于 ConcurrentHashMap,因此能够保证清除、新增缓存的原子性。

群聊接口

这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其他全部客户端都能收到!

流程确定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 SessionSocketHolder 中遍历全部 Channel(通道)而后下发消息便可。

服务端是单机倒也能够,但如今是集群设计。因此全部的客户端会根据以前的轮询算法分配到不一样的 cim-server 实例中。

所以就须要路由层来发挥做用了。


路由接口收到消息后首先遍历出全部的客户端和服务实例的关系。

路由关系在 Redis 中的存放以下:

因为 Redis 单线程的特质,当数据量大时;一旦使用 keys 匹配全部 cim-route:* 数据,会致使 Redis 不能处理其余请求。

因此这里改成使用 scan 命令来遍历全部的 cim-route:*。

接着会挨个调用每一个客户端所在的服务端的 HTTP 接口用于推送消息。

在 cim-server 中的实现以下:


cim-server 收到消息后会在内部缓存中查询该 userID 的通道,接着只须要发消息便可。

在线用户接口

这是一个辅助接口,能够查询出当前在线用户信息。


实现也很简单,也就是查询以前保存 ”用户登陆状态的那个去重 set “便可。

私聊接口

之因此说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。

通常咱们使用私聊的前提确定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。

相似于这样:

在咱们这个场景中,私聊的前提就是须要得到在线用户的 userID。

因此私聊接口在收到消息后须要查询到接收者所在的 cim-server 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 HTTP 接口下发信息。

只是群聊是遍历全部的在线用户,私聊只发送一个的区别。

下线接口

一旦客户端下线,咱们就须要将以前存放在 Redis 中的一些信息删除掉(路由信息、登陆状态)。


IM 客户端

客户端中的一些逻辑其实在上文已经谈到一些了。

登陆

第一步也就是登陆,须要在启动时调用 route 的登陆接口,得到 cim-server 信息再建立链接。

登陆过程当中 route 接口会判断是否为重复登陆,重复登陆则会直接退出程序。

接下来是利用 route 接口返回的 cim-server 实例信息(ip+port)建立链接。

最后一步就是发送一个登陆标志的信息到服务端,让它保持客户端和 Channel 的关系。

自定义协议

上文提到的一些登陆报文、真正的消息报文这些其实都是在咱们自定义协议中能够区别出来的。

因为是使用 Google Protocol Buffer 编解码,因此先看看原始格式。

其实这个协议中目前一共就三个字段:

requestId 能够理解为 userId。

reqMsg 就是真正的消息。

type 也就是上文提到的消息类别。

目前主要是三种类型,分别对应不一样的业务:

心跳

为了保持客户端和服务端的链接,每隔一段时间没有发送消息都须要自动的发送心跳。

目前的策略是每隔一分钟就是发送一个心跳包到服务端:


这样服务端每隔一分钟没有收到业务消息时就会收到 ping 的心跳包:

内置命令

客户端也内置了一些基本命令来方便使用。

好比输入 :q 就会退出客户端,同时会关闭一些系统资源。


当输入 :olu(onlineUser 的简写)就会去调用 route 的获取全部在线用户接口。


群聊

群聊的使用很是简单,只须要在控制台输入消息回车便可。

这时会去调用 route 的群聊接口。

私聊

私聊也是同理,但前提是须要触发关键字;使用 userId;;消息内容 这样的格式才会给某个用户发送消息,因此通常都须要先使用 :olu 命令获取因此在线用户才方便使用。

消息回调

为了知足一些定制需求,好比消息须要保存之类的。

因此在客户端收到消息以后会回调一个接口,在这个接口中能够自定义实现。


所以先建立了一个 caller 的 bean,这个 bean 中包含了一个 CustomMsgHandleListener 接口,须要自行处理只须要实现此接口便可。

自定义界面

因为我本身不怎么会写界面,但保不许有其余大牛会写。因此客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及以后的业务)都是以接口形式提供。

也方便后面作页面集成,只须要调这些接口就好了;具体实现不用怎么关心。

总结

cim 目前只是初版,BUG 多,功能少(只拉了几个群友作了测试);不事后续还会接着完善,至少这一版会给那些没有相关经验的朋友带来一些思路。

以为不错请点赞支持,欢迎留言或进个人我的群855801563领取【架构资料专题目合集90期】、【BATJTMD大厂JAVA面试真题1000+】,本群专用于学习交流技术、分享面试机会,拒绝广告,我也会在群内不按期答题、探讨
相关文章
相关标签/搜索