分布式即时通信系统的设计与实现
2017-09-30杨君徐迪
杨君,徐迪
分布式即时通信系统的设计与实现
杨君,徐迪
(三江学院计算机科学与工程学院,南京 210000)
随着互联网技术的不断发展,即时通信软件的出现大大影响人们之间的交流方式,随着即时通信功能的越来越强大,人们也越来越依赖于即时通信软软件。但是随着用户量的增大,单机系统已经较难支持庞大的用户群,因此将系统进行横向的扩展,即从单机模式转换成分布式模式已经成为即时通信系统的主流趋势。对分布式即时通信系统的研究背景和意义进行阐述,重点阐述系统的详细设计,以及各个模块的功能,并展示已实现的分布式即时通信系统的主要功能。
即时通信;Netty;分布式
0 引言
近年来,随着互联网技术的飞速发展,人们衣食住行依赖于互联网,人们之间的通信也开始依赖于互联网,各种即时通信软件应运而生,如QQ、微信、Skype、MSN,这些即时通信软件的出现使得人们可以随时、随地、随身的交流。截止2016年年底,国内即时通信用户规模已经达到了6.66亿。QQ每日在线人数已经达到了2.5亿之多。
一个系统随着用户量的增大,一味地通过升级硬件来纵向地扩展系统已经没有了可行性,不仅因为成本越来越高,而且由于硬件瓶颈的出现,硬件想要快速升级已经没有了可能性。而且即使拥有一台能够支持所有用户的机器来运行系统,如果这台机器出现了故障,将会导致所有用户都不能够使用,甚至导致数据的丢失等不可挽回的后果。所以横向的扩展系统已经成为了不二之选,理论上可以达到无限的扩展,但是随着扩展的机器增多,机器之间的管理与协作也成了随之而来的问题,可以想象,如果一台机器损坏的几率是1%,只有一台机器的时候,感觉可以接受,但是如果是100台机器组合起来呢,于是出现了许多分布式基础设施来处理和解决这些需求。本课题将从众多基础设施中选出一部分来实现一个简单的分布式即时通信系统。
1 系统设计
本课题实现的即时通信系统总体组成如图2-1所示。客户端理论上可以支持任意客户端,本课题客户端仅基于网页开发,服务端可以在多台机器上部署实现简单的分布式,请求将被转发到不同的服务器上从而实现负载均衡,同时,服务端被分成多个模块,可以在机器上任意部署。
图1 系统总体设计
1.1 客户端
客户端功能如图2所示,可以支持任意客户端,只需建立socket或者websocket即可,本课题仅使用基于网页开发的客户端验证功能。由于本课题重点在于服务端的架构,所以所提供的业务逻辑功能只是一些基础的即时通信的功能,包括注册、登录、初始化、好友列表、发送消息、退出、消息确认等功能。
图2 客户端功能
1.2 服务端
服务端功能如图3所示,服务端将实现websocket的负载均衡,socket连接的管理、消息在不同服务器之间的转发、业务逻辑功能、日志处理等功能。其中业务逻辑功能也就是为客户端提供的所需功能的业务逻辑,如登录、注册、发消息、初始化、退出、消息确认等。
图3 服务端功能
1.3 系统详细设计
整个系统架构如图4所示,为了实现系统的解耦,整个系统被分为8个模块,各个模块各自负责相应功能,模块之间使用MQ实现通信。Nginx负责不同服务之间的负载均衡,数据库将使用Redis作为缓存、MySQL作为持久化存储。
客户端模块为webIM。服务端模块为4个可运行模块和3个依赖模块,可运行模块为webSocketServer、exchangeServer、dealUnit、imLoggerServer,分别负责连接管理、消息在服务器之间的转发、业务逻辑处理、多机日志处理,依赖模块将作为运行模块的依赖包被引入,分别为 imEntity、dealLogic、dataOperation,分别负责整个系统的实体类、业务逻辑处理类、数据库操作。
下面将对各个模块进行消息介绍。
(1)webIM模块
该模块包含与用户交互的Web页面,在webIM中发起socket请求,并解析请求的返回值。
传统的HTTP请求是无法实现被动接受消息的,只能主动的去请求从而获得回复。最容易想到的解决方法就是轮询,即客户端定时向服务器发送AJAX请求,服务器接到请求后马上返回响应信息并关闭连接。毫无疑问这会造成很多无用的请求,浪费带宽和服务器资源。常用的一种解决方式是长轮询,客户端向服务器发送AJAX请求,服务器接到请求后维持住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求,但是这种方法在服务器维持连接时会消耗资源,也难于管理维护。HTML5规范的出现带来了一种全新的解决方案websocket,它可以在浏览器和服务器之间建立TCP连接,可以做到全双工通信,现在大部分浏览器都已经和支持websocket。所以本课题的网页端选择websocket来实现。
图4 发消息流程
由于websocket是异步处理,不是很易于开发,所以在js中对websocket进行了封装。对于请求类消息,客户端并不知道应答会在什么时候返回,只需要在发起请求的同时注册回调函数,在应答返回的时候会自动查询到相应的回调函数进行执行。返回消息和回调的对应关系则是由请求包中的消息序号决定的。对于通知类消息,客户端不知道何时会收到消息,可以在js加载的时候,把对应消息种类的回调注册好,之后便不需要再去关注通知类消息。
(2)webSocketServer模块
使用Netty框架,仅仅使用少数几个线程便可以管理所有websocket的连接和请求。该模块的核心即维护了一个Map,记录了用户的连接和session的对应关系,保证可以通过session找到相应的用户连接。
(3)exchangeServer模块
该模块被用于管理notice消息在不同服务器之间的转发,在收到notice消息后,会检查消息dataPack⁃ageInner包中target字段,从Redis中查询目标用户的所有登录地址,如果是在本机上登录则直接发送给websocketServer,如果不是则根据session中的服务器地址信息找到相应的服务器转发。
(4)dealUnit模块
根据不同的请求命令,通过工厂方法获取dealLog⁃ic中相应的类来处理业务逻辑。该模块的存在使得业务可以任意的横向扩展,当业务模块压力过大时,可以部署多个dealUnit模块,只需在exchangeServer模块做相应的选择,最简单的就是随机选择不同的dealUnit模块。
(5)dealLogic模块
该模块包含了所有请求的业务逻辑,每一种请求命令都有一个对应的处理类,在对应的处理类中会进行相应的数据存取和业务逻辑处理。对于req类消息,会返回一个ack;对于notice消息,不仅会返回一个ack,还会将notice转发给需要发送的目标用户。
(6)dataOperation模块
Redis:主要存放一些缓存数据以及一些少量的id相关数据。
MySQL:主要存放一些需要同步的数据,即Redis中设置了过期时限的数据。
处理数据库请求操作,包括Redis、MySQL,主要的作用在于代理Redis的请求,如果是读操作,查询Re⁃dis后发现其中没有数据,则需要从MySQL中查询出来存入Redis并设置有效期。如果是写操作,并且是需要同步到MySQL中的key则Redis和MySQL中的数据都需要更新。
由于Redis是内存数据库,所以它的速度比关系型数据库MySQL快得多,所以首先会去Redis中查询数据,如果Redis中没有才会去MySQL中查询。但是由于Redis使用的是内存空间,所以除了一些id关系的key,其他的存放较大数据的key都设置了有效期,并在MySQL中做了备份,在Redis中数据过期时,再从MySQL中查询。虽然Redis也有VM,但是其只会在内存不够用的时候才会触发。
(7)imEntity模块
管理所有模块公用的类,如日志发送、常量、Redis⁃Key、工具类等。
(8)imLoggerServer模块
日志收集模块,用RabbitMQ实现的一个简单日志收集模块,启动该模块后,会从MQ获取所有的收集的日志数据交给log4j处理。
在单机的情况下,从imEntity获取的日志记录类只会调用log4j将日志输出到本地;但是log4j并没有提供日志的接口,所以不能实现不同情况使用不同的日志处理,为此slf4j提供了日志的接口使得在不同情况下使用不同的日志处理成为了可能性。在多机情况下,imEntity返回的日志类将会把日志发往MQ中。从而实现了简单的分布式日志的收集。
在开启日志的时候,各个模块会将不同级别的日志发送到MQ的相应级别的队列中,在启动imLog⁃gerServer模块时,会监听相应队列并且做出相应处理,本课题仅仅是将其交给log4j存于本地。
(9)流程实例
各个模块都负责自己相应的功能,下面以一个用户的登录为例,详细说明系统各个模块的运作流程:
①用户打开登录页面,页面被加载,向Nginx发起创建socket连接的请求。
②Nginx接受到了websocket连接请求之后,根据配置,将请求分摊到websocketServer上。
③websocketServer与用户创建socket连接,并将该连接保存下来,之后转发给exchangeServer模块。
④exchangeServer接受到websocketServer发送的请求之后将请求转给dealUnit模块处理。
⑤dealUnit模块接受到请求之后,根据请求的参数,发现是一个登录的请求,调用dealLogic中相应的业务逻辑类来处理这个请求。
⑥dealLogic处理登录请求,从数据库中查询数据,看Redis中是否有用户信息,如果没有还需要从MySQL查询,返回用户的信息给dealUnit模块,deal⁃Unit将返回的数据返回给exchangeServer。
⑦exchangeServer会根据信息的内容决定是否需要转发,由于是req类型的包,所以不需要转发,直接转发给websocketServer。
⑧websocketServer根据返回的内容,从之前存储的连接中找到对应的请求用户的那条连接,将数据返回给用户。
2 系统实现
2.1 系统模块功能
(1)负载均衡
负载均衡通常指将请求按照一定策略分摊到多个处理单元处理,这样也就能够防止单个单元的负载过高,负载均衡可以分为硬负载均衡,软负载均衡。硬负载均衡即使用硬件来实现负载均衡,如F5,虽然硬件有很好的高可用性,但是成本偏高;软负载均衡也就是通过软件来实现负载均衡,常用的有Nginx的反向代理,但是如果发生了单点故障,整个系统从入口就无法进入了,为了防止单点故障,最简单的方法就是配置多台Nginx,通过DNS轮询,将用户的请求分到不同的反向代理中。当然也可以使用LVS来代替,它使用集群技术从Linux系统层面实现了高性能、高可用、负载均衡。但是这些都存在一个问题,它们无法保证把请求分给一个可用的处理单元,也就是说如果后台的一个处理单元发生了故障,它们并不能发现,仍然会将请求分摊过去,这就无法保证高可用性。为了处理这个问题,就需要用到keepalived,它可以用来检测服务状态存活性。
本课题使用Nginx反向代理来实现websocket请求的负载均衡,从而将各个请求分发到了各个web⁃SockerServer模块。
(2)模块间通信
在本课题中,为了降低系统的耦合性,将整个系统拆分成了多个模块,但是各个模块之间的相互通信就成了亟待解决的问题,如果是单机的进程通信,Linux可以使用管道(pipe),但是多台机器之间模块的通信,只能通过手动创建socket去进行数据的交互了,消息队列中间件就是对这一系列问题的解决与完善,内置完善的处理机制,实现了高性能,高可用,可伸缩和最终一致性架构。常用的消息队列有ActiveMQ,Rabbit⁃MQ,ZeroMQ,Kafka,RocketMQ等。消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。
本课题使用RabbitMQ作为消息中间件来实现各个模块的通信和解耦。
(3)分布式缓存
当传统数据库面临大规模数据访问时,磁盘I/O往往成为性能瓶颈,从而导致过高的响应延迟。分布式缓存将高速内存作为数据对象的存储介质,数据以key/value形式存储,理想情况下可以获得内存级的读写性能。
本课题在dataOperation模块中使用Redis作为缓存,从而提高I/O的读写。
(4)分布式日志收集
单机上的日志处理,只需要把日志输出到相应的文件或是其他地方就可以了,有类似于log4j、slf4j等丰富的类库可以通过简单的配置非常容易的实现日志的处理。而一旦转移到多台机器上,日志的收集就会变得复杂。
本课题在imLoggerServer中使用RabbitMQ实现一个非常简单了日志收集模块。
2.2 系统业务逻辑
(1)注册
在用户填写完注册信息之后,会向服务端发起注册请求,服务器在接受到用户名密码之后,会将密码加salt后取得MD5存入到MySQL和Redis中。
(2)登录
服务器在接受到账号密码,验证了账号密码的合法性之后,会将登录者本次连接的session存入Redis,以便别人找到该用户登录的服务器。在socket断开之后,服务端会自动调用下线指令去删除Redis中的ses⁃sion等数据。
(3)初始化
在客户端收到成功登录的ack之后,会发起初始化请求,拉取好友和SYNC序号等信息。
(4)发消息
可以给好友发送消息,支持一个用户同时在多个地方登陆。具体流程如图5所示。
图5 发消息流程
如果用户C1在服务器S1上登录,用户C2在服务器S2上登录,C1发送消息给C2时,服务器C1在收到notice消息之后,会返回给C1一个ack告知客户端服务器成功收到消息,这条消息在经过dealUnit被返回到exchangeServer的时候,exchangeServer查询redis后发现用户C2所登录的服务器不是当前服务器,于是转发给C2所在服务器S2。S2的exchangeServer收到之后转发给了它的websocketServer,最终C2成功接受到了notice消息。
(5)消息确认
针对于notice消息的防丢包机制,由于notice消息只是被动的接受,如果服务器发送了这条消息,但是用户没有接收到,这就导致了消息存在丢失的风险,于是增加了同步(synchronize,sync)机制。
用户在登录时,会返回该用户最新的sync序号,存入本地。服务器在转发notice消息之前,会给这条消息增加一个序号,并将该条消息保存,在客户端接受到消息后,将这条消息的序号与本地的sync消息序号对比,如果发现本地的序号等于消息的序号加一,则说明之前没有丢失的消息,客户端会发送一条特定的req(FIN消息)告知服务器它成功接收到了消息,服务器在接受到该条req后,会返回一个ack,并将那条保存的notice删除。如果客户端发现本地的序号不对,则发送一条req(sync消息),告知服务器将该序号之后的数据再发一遍。
3 结语
本课题主要专注于服务端的架构,结构上实现了负载均衡、模块间通信、连接管理、分布式缓存、分布式日志收集等功能,实现了服务端的横向扩展,业务逻辑上则实现了一些简单的即时通信系统功能,如注册、登录、初始化、好友信息、发送消息、消息确认等功能。
[1]李代立,陈榕.WebSocket在Web实时通信领域的研究[J].电脑知识与技术,2010,06(28):7923-7925
[2]李璐,张广泉.消息中间件的体系结构研究[J].苏州大学学报(工科版),2007,27(3):10-14
[3]代超,邓中亮.基于Netty的面向移动终端的推送服务设计[J].软件,2015(12):1-4
[4]曾超宇,李金香.Redis在高速缓存系统中的应用[J].微型机与应用,2013,32(12):11-13
[5]刘影,季波.企业级即时通信系统的应用研究[J].现代商贸工业,2009,19(20):36-36
Design and Implementation of Distributed Instant Messaging System
YANG Jun,XU Di
(School of Computer Science and Engineering,Sanjiang University,Nanjing 210000)
With the continuous development of Internet technology,instant communication software has greatly affected the communication between the people,with the instant communication function more and more powerful,more and more people rely on instant communication soft⁃ware.But with the increase of the number of users,the stand-alone system has been difficult to support the huge user base,so for the hori⁃zontal expansion of the system,from the stand-alone mode into a distributed mode has become the mainstream trend of instant messaging system.Describes the research background and significance of the distributed instant messaging system,and then focuses on the detailed design of the system,as well as the function of each module.Finally,presents the main functions of the distributed instant messenger sys⁃tem.
Instant Messaging;Netty;Distributed System
1007-1423(2017)24-0071-05
10.3969/j.issn.1007-1423.2017.24.017
杨君,讲师,研究方向为分布式计算
徐迪(1995-),男,江苏南京人,工程师,研究方向为分布式系统、软件工程
2017-05-25
2017-08-12