无论是IM消息通信系统还是客户消息系统,其本质都是一套消息发送与投递系统,或者说是一套网络通信系统,其本质两个词:存储与转发。
根据个人理解,其应有的feature如下
A 整个系统中Server端提供存储转发能力,无论整体架构是B/S还是C/S;
B 消息发送者能够成功发送消息给后端,且得到后端地确认;
C 接收端能够不重不漏地接收Server端转发来的没有超过消息生命周期和系统承载能力的消息;
D 整个系统只考虑文本短消息[即限制其长度];
E 每条消息都有生命周期,如一天,且有长度限制如1440B【尽量不要超过一个frame的size】,只考虑在线消息的处理,无论是超时的消息还是超出系统承载能力的消息[如键盘狂人或者键盘狂机器人发出的消息]都被认为是"垃圾消息";
F 为简单起见,不给消息很多类型,如个人对个人消息,群消息,讨论组消息等,都认为是一种群[下文用channel替代之,也有人用Room这个词]消息类型;
G 为简单起见,这个群的建立与销毁流程本文不述及,也即消息流程开始的时候各个消息群都已经组建完毕,且流程中没有成员的增减;
H 账户申请、用户鉴权和天朝独有的黄反词检查等IM安全层等暂不考虑。
系统名词解释
1 PC: 单机型客户端,如windows端和mac端等等;
2 Web/h5: 网页客户端;
3 Android:手机移动端,取其典型Android端,当然也有ios端[但是考虑到各家开发App都是安卓客户端最先上系统新版本,故用Android代表之];
4 broker:文本消息的有线或者无线接口端,考虑到携程采用了这个词,我也姑且先用之,它提供了消息的接收与投递功能;
5 Relay:图片/语音/视频 转发接口端,其后端可以是自家的服务也可以是第三方服务(如提供图片存储服务的七牛、提供云视频解决方案的腾讯云等);
6 msg chat server:消息逻辑处理端;
7 Router: 在线状态服务端,存储在线的用户以及其登录的broker接口机的id以及一些心跳包时间等数据;
8 Counter: 消息计数器,为每个text等类型的消息分配MSG id;
9 Msg Queue: 每个channel消息的msg id队列,存储每个client未接收的且未超时的且未超出队列大小的msg id集合;
10 Mysql/mongodb: 消息存储服务、用户资料数据、以及channel成员列表服务数据库,因为二者比较典型,所以取用了这个名字,当然你可以在其上部署一层cache服务;
11 Client:客户端层;
12 Interface/If(下文简称If):服务接口层;
13 Logic:消息逻辑处理层,[这层其实应该有系统最多的模块];
14 DB:存储层,存储了在线状态、消息id以及msg id队列和消息内容等;
15 http: 消息发送和接收协议,IM协议中一般理解为long polling消息处理方式,在web端多采用这种协议;
16 Websocket: 另一种消息发送和接收协议,在移动环境或者采用html5开发的系统多采用这种协议;
17 TCP: 另一种消息发送和接收协议,在环境或者采用html5开发的系统多采用这种协议;
18 UDP: 另一种消息发送和接收协议,某个不保证提供稳定消息传输服务的厂家采用的协议,也许也是用户最多使用的协议,它的优点是无论是无线还是有线环境下都非常快,又由于http/Websocket的基础都是tcp协议,UDP协议在环境拥塞情况下由于不提供拥塞控制等退让算法,反而会去争用网络通道,所以在网络复杂的特别是发生网络风暴的情况下它会显得更快^ _ ^ & ^ _ ^【呵呵哒】;
19 RPC: 一种远程过程调用协议,提供分布式环境下的函数调用能力;
20 Restful: 一种远程服务提供的架构风格,跟RPC比起来貌似更高级点。
在介绍消息发送流程之前,先介绍一些基本概念。即时通讯聊天软件app开发可以加蔚可云的v:weikeyun24咨询
一个消息系统,从宏观上来说,就是一个PUB/SUB系统,有消息生成者publisher[or producer],有消息中转者broker,有消息处理者msg server,以及消息消费者subscriber[or consumer]。消息消费者可以是一个人,也可以是一群人,在pub/sub系统之中producer&consumer一起构成了一个channel,或者称之为room,或者称之为group。
无论是producer还是consumer,每个具体单位都要由系统分配给一个id,称之为UIN[名词来源于icq]。
后端的if层的broker机器可以在全球或者某个区域分布多个,UIN依据dns系统可以得到if层所有的机器列表,如果dns层由于机器坏掉或者是被攻击时不能服务,那么客户端应该根据记忆[无论是上次成功登陆的机器还是被厂家内置的机器列表]知道某些机器的ip&port地址,然后根据测速结果来选择一个离其最近的broker。
UIN在于broker之间进行一段时间内有效的会话服务,称之为一个session。这个session存活于一个长连接里,也可以横跨几个长连接或者短连接,即session自身依赖的网络链接是不稳定的。session有效期间内,Server认为UIN在线,session有效期内客户端要定时地给broker发送心跳包。本文认为的session可以是不稳定的,即session有效期内下发给客户端的消息可以丢失,但是可以通过一些其他手段保证消息被投递给客户端。
消息的制造者[producer]一般是IM系统的最基本单元UIN[即一个自然人],既然是一个自然人,就认为其发送能力有限,不可能一秒内发出多于一条的消息,即其消息频率最高为: 1条msg / s。高于这个频率,都被认为是键盘狂人或者狂躁机器人,客户端或者服务端应该具有拒绝给这种人提供服务或者丢弃其由于发狂而发出的消息。
基于上面这个假设,producer发出的消息请求被称为msg req,服务器给客户端返回的消息响应称为msg ack。整个消息流程为:
A client以阻塞方式发出msg req,req = {producer uin, channel name, msg device id, msg time, msg content};
B broker收到消息后,以uin为hash或者通过其他hash方式把消息转发给某个msg chat server;
C msg chat server收到消息后以key = Hash{producer uin【发送者id】 + msg device id【设备id】+ msg time【消息发送时间,精确到秒】}到本地消息缓存中查询消息是否已经存在,如果存在则终止消息流程,给broker返回"duplicate msg"这个msg ack,否则继续;
D msg chat server到Counter模块以channel name为key查询其最新的msg id,把msg id自增一后作为这条消息的id;
E msg chat server把分配好id的消息插入本地msg cache和msg DB[mysql/mongoDB]中;
F msg chat server给broker返回msg ack, ack = {producer uin, channel name, msg device id, msg time, msg id};
G broker把msg ack下发给producer;
H producer收到ack包后终止消息流程,如果在发送流程超时后仍未收到消息则转到步骤1进行重试,并计算重试次数;
I 如果重试次数超过两次依然失败则提示“系统繁忙” or “网络环境不佳,请主人稍后再尝试发送”等,终止消息发送流程。
上面设计到了一个模块图中没有的概念:msg cache,之所以没有绘制出来,是因为msg cache的大小是可预估的,它只是用于消息去重判断,所以只需存下去重msg key即可。假设msg chat server的服务人数是40 000人,消息发送频率是1条/s,消息的生命周期是24 hour,消息key长度是64B,那么这个cache大小 = 64B * (24 * 3600)s * 40000 = 221 184 000 000B,这个数字可能有点恐怖,如果是真实商业环境这个数字只会更小,因为没有人一天一夜不吃不喝不停发消息嘛。其本质是一个hashset(C++中对应的是unordered_set),物理存储介质当然是共享内存了。
[2016/03/10日:经过思考,msg cache只需存下某个UIN在某个device上的最新的消息时间即可,msg cache的结构应为hashtable,以{UIN + device id}为key,以其最新的消息的发送时间(客户端发送消息的时间)为value,不再考虑消息的生命周期。msg chat server每收到一条新消息就把新消息中记录的发送时间与缓存中记录的消息时间比较即可,如果新消息的时间小于这个msg pool记录的时间即说明其为重复消息,大于则为新消息,并用新消息的msg time作为msg cache中对应kv的value的最新值。假设UIN为4B,device id为4B,时间为4B,则msg cache的数据的size(不计算hashtable数据结构本身占用的内存size)为12B * 40000 = 480 000B,新msg pool完全与每条消息的lifetime无关,这就大大下降了其内存占用。
那么还有一个问题,如果用户修改了手机的本地时间怎么办?那就换做另一个参数:本地手机时钟累计运行时长,手机出厂后其运行累计时长只会一直增加不会减小。
这个流程牵涉到一个比较重要的模块:Counter,这个模块其实都可以用Redis充当,怎么做你自己想^ _ ^。这个模块自身的实现就是一个分布式的计数器,直接使用Redis也没什么问题,但是最好的方法是采用消息id批发器的方式,msg chat server到Counter每次批发一批id回来,然后分配给每个msg,当使用完毕的时候再接着去Counter申请一批回来,以减轻Counter的压力,具体的设计请参考专利《即时消息的处理方法和装置》[参考文档9]。
上面还有一个概念未叙述到:发送端的消息邮箱{有人称为消息盒子,或者某大厂称之为客户端消息db},它存储了所有本地发送出去的消息,其中没有服务端分配的msg id的消息都被认为是发送失败的消息,待用户主动尝试发送或者网络环境重新稳定后可以有客户端尝试重新发送流程。
用户查看消息邮箱中的本地历史消息的时候,就要依据msg id把消息排序好展现给用户。至于用户发送过程中看到的消息可以认为是本地消息的一个cache,每个channel最多给他展现100条,这100条消息的排序要依照每条消息的发出时间或者是消息的接收时间[这个接收到的消息时间以消息到达本机时的本地时钟为依据]。当用户要查看超出数目如100条消息之外的消息,客户端要引导用户去走历史消息查看流程。