本文原题“搭建高性能的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... )