关于即时通讯:跟着源码学IM十基于Netty搭建高性能IM集群含技术思路源码

62次阅读

共计 8534 个字符,预计需要花费 22 分钟才能阅读完成。

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

正文完
 0