跟着源码学IM(十):基于Netty,搭建高性能IM集群(含技术思路)
yuyutoo 2024-12-16 17:22 3 浏览 0 评论
本文原题“搭建高性能的IM系统”,作者“刘莅”,内容有修订和改动。
1、引言
相信很多朋友对微信、QQ等聊天软件的实现原理都非常感兴趣,笔者同样对这些软件有着深厚的兴趣。而且笔者在公司也是做IM的,公司的IM每天承载着上亿条消息的发送!
正好有这样的技术资源和条件,所以前段时间,笔者利用业余时间,基于Netty开发了一套基本功能比较完善的IM系统。该系统支持私聊、群聊、会话管理、心跳检测,支持服务注册、负载均衡,支持任意节点水平扩容。
这段时间,网上的一些读者,也希望笔者分享一些Netty或者IM相关的知识,所以今天笔者把开发的这套IM系统分享给大家。
本文将根据笔者这次的业余技术实践,为你讲述如何基于Netty+Zk+Redis来搭建一套高性能IM集群,包括本次实现IM集群的技术原理和实例代码,希望能带给你启发。
2、本文源码
主地址:https://github.com/nicoliuli/chat
备地址: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、系统架构
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性能到底有没有比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、参考资料
[6] 理论联系实际:一套典型的IM通信协议设计详解
[7] 浅谈IM系统的架构设计
[8] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[9] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
[10] 一套原创分布式即时通讯(IM)系统理论架构方案
[11] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践
[12] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
[13] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等
[14] 从新手到专家:如何设计一套亿级消息量的分布式IM系统
[15] 基于实践:一套百万消息量小规模IM系统技术要点总结
学习交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK
(本文已同步发布于:http://www.52im.net/thread-3816-1-1.html )
相关推荐
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
-
Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
-
概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...
- MySQL CREATE TABLE 简单设计模板交流
-
推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....
- mysql学习9:创建数据库(mysql5.5创建数据库)
-
前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
-
执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
-
在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
-
多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
-
全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...
- 【推荐】ProAc Response系列扬声器逐个看
-
有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
-
作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...
- 专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)
-
Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...
- 常用半导体中英对照表(建议收藏)(半导体英文术语)
-
作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...
- Fyne Audio F502SP 2.5音路低音反射式落地音箱评测
-
FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...
- 有哪些免费的内存数据库(In-Memory Database)
-
以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...
- RazorSQL Mac版(SQL数据库查询工具)
-
RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
- MySQL CREATE TABLE 简单设计模板交流
- mysql学习9:创建数据库(mysql5.5创建数据库)
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
- 【推荐】ProAc Response系列扬声器逐个看
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)