本文原题“搭建高性能的 IM 零碎”,作者“刘莅”,内容有订正和改变。为了尊重原创,如需转载,请分割作者取得受权。
1、引言
置信很多敌人对微信、QQ 等聊天软件的实现原理都十分感兴趣,笔者同样对这些软件有着深厚的趣味。而且笔者在公司也是做 IM 的,公司的 IM 每天承载着上亿条音讯的发送!
正好有这样的技术资源和条件,所以前段时间,笔者利用业余时间,基于 Netty 开发了一套基本功能比较完善的 IM 零碎。该零碎反对私聊、群聊、会话治理、心跳检测,反对服务注册、负载平衡,反对任意节点程度扩容。
这段时间,网上的一些读者,也心愿笔者分享一些 Netty 或者 IM 相干的常识,所以明天笔者把开发的这套 IM 零碎分享给大家。
本文将依据笔者这次的业余技术实际,为你讲述如何基于 Netty+Zk+Redis 来搭建一套高性能 IM 集群,包含本次实现 IM 集群的技术原理和实例代码,心愿能带给你启发。
学习交换:
- 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
- 开源 IM 框架源码:https://github.com/JackJiang2…
(本文已同步公布于:http://www.52im.net/thread-38…)
2、本文源码
主地址:https://github.com/nicoliuli/…
备地址:https://github.com/52im/chat
源码的目录构造,如下图所示:
3、常识筹备
- 重要提醒:本文不是一篇即时通讯实践文章,文章内容来自代码实战,如果你对即时通讯(IM)技术实践理解的太少,倡议先具体浏览:《新手入门一篇就够:从零开发挪动端 IM》。
可能有人不晓得 Netty 是什么,这里简略介绍下:
Netty 是一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以疾速开发高性能、高可靠性的网络服务器和客户端程序。
援用
也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,应用 Netty 能够确保你疾速和简略的开发出一个网络应用,例如实现了某种协定的客户,服务端利用。
援用
Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 Socket 服务开发。
以下是无关 Netty 的入门文章:
1)新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析
2)写给初学者:Java 高性能 NIO 框架 Netty 的学习办法和进阶策略
3)史上最艰深 Netty 框架入门长文:根本介绍、环境搭建、入手实战
如果你连 Java 的 NIO 都不晓得是什么,上面的文章倡议优先读:
1)少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别
2)史上最强 Java NIO 入门:放心从入门到放弃的,请读这篇!
3)Java 的 BIO 和 NIO 很难懂?用代码实际给你看,再不懂我转行!
Netty 源码和 API 的在线查阅地址:
1)Netty-4.1.x 残缺源码(在线浏览版)
2)Netty-4.1.x API 文档(在线版)
4、零碎架构
零碎的架构如上图所示:整个零碎是一个 C / S 零碎,客户端没有做简单的图形化界面而是用 Java 终端开发的(黑窗口),服务端 IM 实例是 Netty 写的 socket 服务。
ZK 作为服务注册核心,Redis 用来做分布式会话的缓存,并保留用户信息和轻量级的音讯队列。
对于整个零碎架构中各局部的工作原理,咱们将在接下来的各章节中一一介绍。
5、服务端的工作原理
在上述架构中:NettyServer 启动,每启动一台 Server 节点,都会把本身的节点信息,如:ip、port 等信息注册到 ZK 上(长期节点)。
正如上节架构图上启动了两台 NettyServer,所以 ZK 上会保留两个 Server 的信息。
同时 ZK 将监听每台 Server 节点,如果 Server 宕机 ZK 就会删除以后机器所注册的信息(把长期节点删除),这样就实现了简略的服务注册的性能。
6、客户端的工作原理
Client 启动时,会先从 ZK 上随机抉择一个可用的 NettyServer(随机示意能够实现负载平衡),拿到 NettyServer 的信息(IP 和 port)后与 NettyServer 建设链接。
链接建设起来后,NettyServer 端会生成一个 Session(即会话),用来把以后客户端的 Channel 等信息组装成一个 Session 对象,保留在一个 SessionMap 里,同时也会把这个 Session 保留在 Redis 中。
这个会话特地重要,通过会话,咱们能获取以后 Client 和 NettyServer 的 Channel 等信息。
7、Session 的作用
咱们启动多个 Client,因为每个 Client 启动,都会先从 ZK 上随机获取 NettyServer 的的信息,所以如果启动多个 Client,就会连贯到不同的 NettyServer 上。
相熟 Netty 的敌人都晓得,Client 与 Server 建设接连后会产生一个 Channel,通过 Channel,Client 和 Server 能力进行失常的网络数据传输。
如果 Client1 和 Client2 连贯在同一个 Server 上:那么 Server 通过 SessionMap 别离拿到 Client1 和 Client2 的会话,会话中蕴含 Channel 信息,有了两个 Client 的 Channel,Client1 和 Client2 便可实现音讯通信。
如果 Client1 和 Client2 连贯到不同的 NettyServer 上:Client1 和 Client2 要进行通信,该怎么办?这个问题放在前面解答。
8、高效的数据传输
无论是 IM 零碎,还是分布式的 RPC 框架,高效的网络数据传输,无疑会极大的晋升零碎的性能。
数据通过网络传输时,个别把对象通序列化成二进制字节流数组,而后将数据通过 socket 传给对方服务器,对方服务器拿到二进制字节流后再反序列化成对象,达到近程通信的目标。
在 Java 畛域,Java 序列化对象的形式有重大的性能问题,业界罕用谷歌的 protobuf 来实现序列化反序列化(见《Protobuf 通信协议详解:代码演示、具体原理介绍等》)。
protobuf 反对不同的编程语言,能够实现跨语言的零碎调用,并且有着极高的序列化反序列化性能,本零碎也采纳 protobuf 来做数据的序列化。
对于 Protobuf 的根本认之,上面这几篇能够深刻读一读:
《强列倡议将 Protobuf 作为你的即时通讯利用数据传输格局》
《全方位评测:Protobuf 性能到底有没有比 JSON 快 5 倍?》
《金蝶顺手记团队分享:还在用 JSON? Protobuf 让数据传输更省更快(原理篇)》
另外:《一套海量在线用户的挪动端 IM 架构设计实际分享(含具体图文)》一文中,“3、协定设计”这一节有对于 protobuf 在 IM 中的实战设计和应用,能够一并学习一下。
9、聊天协定定义
咱们在应用各种聊天 APP 时,会发各种各样的音讯,每种音讯都会对应不同的音讯格局(即“聊天协定”)。
聊天协定中次要蕴含几种重要的信息:
1)音讯类型;
2)发送工夫;
3)音讯的收发人;
4)聊天类型(群聊或私聊)。
我的这套 IM 零碎中,聊天协定定义如下:
syntax = "proto3";
option java_package = "model.chat";
option java_outer_classname = "RpcMsg";
message Msg{
string msg_id = 1;
int64 from_uid = 2;
int64 to_uid = 3;
int32 format = 4;
int32 msg_type = 5;
int32 chat_type = 6;
int64 timestamp = 7;
string body = 8;
repeated int64 to_uid_list = 9;
}
如下面的 protobuf 代码,字段的具体含意如下:
1)msg_id:示意音讯的惟一 id,能够用 UUID 示意;
2)from_uid:音讯发送者的 uid;
3)to_uid:音讯接收者的 uid;
4)format:音讯格局,咱们应用各种聊天软件时,会发送文字音讯,语音音讯,图片音讯等等等等,每种音讯有不同的音讯格局,咱们用 format 来示意(因为本零碎是 java 终端,format 字段没有太大含意,可有可无);
5)msg_type:音讯类型,比方登录音讯、聊天音讯、ack 音讯、ping、pong 音讯;
6)chat_type:聊天类型,如群聊、私聊;
7)timestamp:发送音讯的工夫戳;
8)body:音讯的具体内容,载体;
9)to_uid_list:这个字段用户群聊音讯进步群聊音讯的性能,具体作用会在群聊原理局部具体解释。
10、私聊音讯发送原理
Client1 给 Client2 发消息时,咱们须要构建上节中的音讯体。
具体就是:from_uid 是 Client1 的 uid、to_uid 是 Client2 的 uid。
NettyServer 收到音讯后的解决逻辑是:
1)解析到 to_uid 字段;
2)从 SessionMap 或者 Redis 中保留的 Session 汇合中获取 to_uid 即 Client2 的 Session;
3)从 Session 中取出 Client2 的 Channel;
4)而后将音讯通过 Client2 的 Channel 发给 Client2。
11、群聊音讯发送原理
群聊音讯的散发通常有两种技术实现形式,咱们一一来看看。
形式一:假如一个群有 100 人,如果 Client1 给一个群的所有人发消息,其实相当于 Client1 别离给其余 99 人别离发一条音讯。咱们能够间接在 Client 端,通过循环,别离给群里的 99 人发消息即可,相当于 Client 发送给 NettyServer 发送了 99 次雷同的音讯(除了 to_uid 不同)。
上述计划有很重大的性能问题:Client1 通过循环 99 次,别离把音讯发给 NettyServer,NettyServer 收到这 99 条音讯后,别离将音讯发给群内其余的用户。先抛开挪动端的特殊性(比方循环还没实现手机就有可能退到后盾被零碎挂起),显然 Client1 到 NettyServer 的 99 次循环存在显著不合理中央。
形式二:上节的音讯体中 to_uid_list 字段就是为了解决这个形式一的性能问题的。Client1 把群内其余 99 个 Client 的 uid 保留在 to_uid_list 中,而后 NettyServer 只发一条音讯,NettyServer 收到这一条音讯后,通过 to_uid_list 字段解析群内其余 99 的 Client 的 uid,再通过循环把音讯别离发送给群内其余的 Client。
能够看到:形式二的群聊时,Client1 与 NettyServer 只进行 1 次音讯传输,相比于形式一,效率进步了 50%。
11、技术关键点 1:客户端别离连贯在不同 IM 实例时如何通信?
针对本文中的架构,如果多个 Client 别离连贯在不同的 Server 上,Client 之间应该如何通信呢?
为了答复这个问题,咱们首先要明确 Session 的作用。
咱们做过 JavaWeb 开发的敌人都晓得,Session 用来保留用户的登录信息。
在 IM 零碎中也是如此:Session 中保留用户的 Channel 信息。当 Client 与 Server 建设链接胜利后,会产生一个 Channel,Client 和 Server 是通过 Channel,实现数据传输。当两端链接建设起来后,Server 会构建出一个 Session 对象,保留 uid 和 Channel 等信息,并把这个 Session 保留在一个 SessionMap 里(NettyServer 的内存里),uid 为 key,咱们能够通过 uid 就能够找到这个 uid 对应的 Session。
但只有 SessionMap 还不够:咱们须要利用 Redis,它的作用是保留整个 NettyServer 集群全副链接胜利的用户,这也是一种 Session,但这种 Session 没有保留 uid 和 Channel 的对应关系,而是保留 Client 链接到 NettyServer 的信息,如 Client 链接到的这个 NettyServer 的 ip、port 等。通过 uid,咱们同样能够从 Redis 中拿到以后 Client 链接到的 NettyServer 的信息。正是有了这个信息,咱们能力做到,NettyServer 集群任意节点程度扩容。
当用户量少的时候:咱们只须要一台 NettyServer 节点便能够扛住流量,所有的 Client 链接到同一个 NettyServer 上,并在 NettyServer 的 SessionMap 中保留每个 Client 的会话。Client1 与 Client2 通信时,Client1 把音讯发给 NettyServer,NettyServer 从 SessionMap 中取出 Client2 的 Session 和 Channel,将音讯发给 Client2。
随着用户量一直增多:一台 NettyServer 不够,咱们减少了几台 NettyServer,这时 Client1 链接到 NettyServer1 上并在 SessionMap 和 Redis 中保留了会话和 Client1 的链接信息,Client2 链接到 NettyServer2 上并在 SessionMap 和 Redis 中保留了会话和 Client2 的链接信息。Client1 给 Client2 发消息时,通过 NettyServer1 的 SessionMap 找不到 Client2 的会话,音讯无奈发送,于是便从 Redis 中获取 Client2 链接在哪台 NettyServer 上。获取到 Client2 所链接的 NettyServer 信息后,咱们能够把音讯转发给 NettyServer2,NettyServer2 收到音讯后,从 NettyServer2 的 SessionMap 中获取 Client2 的 Session 和 Channel,而后将音讯发送给 Client2。
那么:NettyServer1 的音讯如何转发给 NettyServer2 呢?答案是通过音讯队列,如 Redis 中的 list 数据结构。每台 NettyServer 启动后都须要监听一个本人的 Redis 中的音讯队列,这个队列用户接管其余 NettyServer 转发给以后 NettyServer 的音讯。
- Jack Jiang 点评:上述集群计划中,Redis 既作为在线用户列表存储核心,又作为集群中不同 IM 长连贯实例的音讯直达服务(此时的 Redis 作用相当于 MQ),那 Redis 不就成为了整个分布式集群的单点瓶颈了吗?
12、技术关键点 2:链接断开,如何解决?
如果 Client 与 NettyServer,因为某种原因(客户端退出、服务端重启、网络因素等)断开链接,咱们必须要从 SessionMap 删除会话和 Redis 中保留的数据。
如果不革除这两类数据的话,很有可能 Client1 发送给 Client2 的音讯,可能会发给其余用户,或者就算 Client2 处于登录状态,Client2 也收到不到音讯。
咱们能够在 Netty 框架中的 channelInactive 办法里,解决链接断开后的会话革除操作。
13、技术关键点 3:ping、pong 的作用
当 Client 与 NettyServer 建设链接后,因为双端网络较差,Client 与 NettyServer 断开链接后,如果 NettyServer 没有感知到,也就没有革除 SessionMap 和 Redis 中的数据,这将会造成重大的问题(对于服务端来说,这个 Client 的会话理论处于“假死”状态,音讯是无奈实时发送过来的)。
此时就须要一种 ping/pong 机制(也就是心跳机制啦)。
实现原理就是:通过定时工作,Client 每隔一段时间给 NettyServer 发一个 ping 音讯,NettyServer 收到 ping 音讯后给客户端回复一个 pong 音讯,确保客户端和服务端能始终放弃链接状态。如果 Client 与 NettyServer 断连了,NettyServer 能够立刻发现并清空会话数据。Netty 中的咱们能够在 Pipeline 中增加 IdleStateHandler,可达到这样的目标。
如果你不明确心跳的作用,务必读以下文章:
《为何基于 TCP 协定的挪动端 IM 依然须要心跳保活机制?》
《一文读懂即时通讯利用中的网络心跳包机制:作用、原理、实现思路等》
也能够学习一下支流 IM 的心跳逻辑:
《微信团队原创分享:Android 版微信后盾保活实战分享(过程保活篇)》
《微信团队原创分享:Android 版微信后盾保活实战分享(网络保活篇)》
《挪动端 IM 实际:实现 Android 版微信的智能心跳机制》
《挪动端 IM 实际:WhatsApp、Line、微信的心跳策略剖析》
如果感觉实践不够直观,上面的代码实例能够直观地进行学习:
《正确理解 IM 长连贯的心跳及重连机制,并入手实现(有残缺 IM 源码)》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨(含样例代码)》
《自已开发 IM 有那么难吗?手把手教你自撸一个 Andriod 版繁难 IM (有源码)》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
其实,心跳算法的实际效果,还是有一些逻辑技巧的,以下两篇倡议必读:
《Web 端即时通讯实际干货:如何让你的 WebSocket 断网重连更疾速?》
《融云技术分享:融云安卓端 IM 产品的网络链路保活技术实际》
14、技术关键点 4:为 Server 和 Client 增加 Hook
如果 NettyServer 重启了或者过程被 kill 掉,咱们须要革除以后节点的 SessionMap(其实不必清理 SessionMap,数据在内存里重启会主动删除的)和 Redis 保留的 Client 的链接信息。
咱们须要遍历 SessionMap 找出所有的 uid,而后一一革除 Redis 的数据,而后优雅退出。此时,咱们就须要为咱们的 NettyServer 增加一个 Hook,来做数据清理。
15、技术关键点 5:对方不在线该如何解决音讯?
Client1 给对方发消息,咱们通过 SessionMap 或 Redis 拿不到对方的会话数据,这就表明对方不在线。
此时:咱们须要把音讯存储在离线音讯表中,当对方下次登录时,NettyServer 查离线音讯表,把音讯发给登录用户(最好是批量发送,进步性能)。
IM 中的离线音讯解决,也不是个简略的技术点,有趣味能够深刻学习一下:
《IM 音讯送达保障机制实现(二):保障离线音讯的牢靠投递》
《阿里 IM 技术分享(六):闲鱼亿级 IM 音讯零碎的离线推送达到率优化》
《IM 开发干货分享:我是如何解决大量离线音讯导致客户端卡顿的》
《IM 开发干货分享:如何优雅的实现大量离线音讯的牢靠投递》
《喜马拉雅亿级用户量的离线音讯推送零碎架构设计实际》
16、写在最初
代码写成这样,也算是了确了自已手撸 IM 的宿愿。惟一遗憾的是,工夫比拟缓和,还没来得及实现音讯 ack 机制,保障音讯肯定会送达,这个笔者当前会补充下来的。
好了,这就是我开发的这个繁难的聊天零碎,麻雀虽小,五脏俱全,大家有什么不明确的中央,能够间接在下方留言,笔者会一一回复的,谢谢大家。
17、系列文章
《跟着源码学 IM(一):手把手教你用 Netty 实现心跳机制、断线重连机制》
《跟着源码学 IM(二):自已开发 IM 很难?手把手教你撸一个 Andriod 版 IM》
《跟着源码学 IM(三):基于 Netty,从零开发一个 IM 服务端》
《跟着源码学 IM(四):拿起键盘就是干,教你徒手开发一套分布式 IM 零碎》
《跟着源码学 IM(五):正确理解 IM 长连贯、心跳及重连机制,并入手实现》
《跟着源码学 IM(六):手把手教你用 Go 疾速搭建高性能、可扩大的 IM 零碎》
《跟着源码学 IM(七):手把手教你用 WebSocket 打造 Web 端 IM 聊天》
《跟着源码学 IM(八):万字长文,手把手教你用 Netty 打造 IM 聊天》
《跟着源码学 IM(九):基于 Netty 实现一套分布式 IM 零碎》
《跟着源码学 IM(十):基于 Netty,搭建高性能 IM 集群(含技术思路 + 源码)》(* 本文)
18、参考资料
[1] 新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析
[2] 写给初学者:Java 高性能 NIO 框架 Netty 的学习办法和进阶策略
[3] 史上最强 Java NIO 入门:放心从入门到放弃的,请读这篇!
[4] Java 的 BIO 和 NIO 很难懂?用代码实际给你看,再不懂我转行!
[5] 史上最艰深 Netty 框架入门长文:根本介绍、环境搭建、入手实战
[6] 实践联系实际:一套典型的 IM 通信协议设计详解
[7] 浅谈 IM 零碎的架构设计
[8] 简述挪动端 IM 开发的那些坑:架构设计、通信协议和客户端
[9] 一套海量在线用户的挪动端 IM 架构设计实际分享(含具体图文)
[10] 一套原创分布式即时通讯(IM) 零碎实践架构计划
[11] 一套高可用、易伸缩、高并发的 IM 群聊、单聊架构方案设计实际
[12] 一套亿级用户的 IM 架构技术干货(上篇):整体架构、服务拆分等
[13] 一套亿级用户的 IM 架构技术干货(下篇):可靠性、有序性、弱网优化等
[14] 从老手到专家:如何设计一套亿级音讯量的分布式 IM 零碎
[15] 基于实际:一套百万音讯量小规模 IM 零碎技术要点总结
(本文已同步公布于:http://www.52im.net/thread-38…)