【图文直播全文记录】关于im的一切 (上)

此文根据【QCON高可用架构群】分享内容,由群内【编辑组】志愿整理,转发请注明出处。

沈剑,目前任58同城技术委员会主席,高级架构师,优秀讲师。负责过百度hi,58帮帮等im系统的架构设计。同时,欢迎大家关注沈剑讲师的微信公众号“架构师之路”。

一、什么是IM

1、IM概述

IM 是“instant messaging”的简称,翻译成即时通讯。说到即时通讯,我们可能最先想到的是一款叫 ICQ的聊天软件 ,后来者还有微信/skype/msn/momo等 。IM包含即时和通讯是两个关键词。

所以“即时通讯”,从字面上看,就是快速,按照一定协议交换信息。 下图是一个ICQ的截图:


2、IM系统特性

im系统相对其他系统而言有自己的几个特点:

以股市变化曲线为例,客户端不主动发送请求,曲线会随时间自动变化 :


消息可达性即消息的可靠投递 ,有一个著名的定理:SMC定理,Single-Message Communication,Published in : Communications, IEEE Transactions on (Volume:24 , Issue: 2 ) ,很短的一个论文,文章的结论是,任何端到端的消息传递协议 ,消息既不丢失,同时也不重复是不可能的。后续会分享,系统层面的重复,可以换取业务层面的不丢失和不重复。


大部分人可能了解XMPP(Extensible Messaging and Presence Protocol )协议 ,即可扩展消息和Presence协议 ,这个Presence直接翻译就是“出席“,何为出席? 大家都到群内签到了,叫出席 ,没来听讲座叫缺席 ,在IM中出席缺席表示状态 。登录在线叫出席,没登录叫缺席 。
几乎所有的IM请求都和状态相关(比如:在线转发,不在线存离线) ,所以状态的一致性尤为重要 。

举个例子:

假设用户A有100个好友,用户A加入了10个群,每个群有100个人。用户A在登录的一瞬间,状态由“不在线”变为了“在线”,他的状态的变更需要通知要通知1100个人,包括100个好友和1000个群友,这个扩散系数是非常庞大,

如何做这1100个人的状态推送? 常见的做法和Feed,微博类似 ,你的100个好友(实时性要求很高),采用推的模式 ,你的1000个群友采用拉的模式(实时性要求不高) 。当然,这里还只是说状态很很复杂,还没涉及到状态一致性问题。

二、协议设计

网络协议是由三个要素组成,语义、语法、时序。语义表示要做什么,语法表示要怎么做,时序表示做的顺序。 本文主要讲解重点语法的设计。

IM的协议分为3层,如下图:

1、应用层

常用的IM应用层协议有3种 :

1.1、文本协议

文本协议是“贴近人类书面语言”的协议,典型例子是MSN ,另外HTTP协议也是文本协议,如下图所示:

文本协议有几个特点:

1.2、 二进制协议

二进制协议最典型的是IP协议,如下图所示:

二进制协议一般定长包头和可扩展变长包体 ,每个字段固定了含义 ,例如IP协议的前4个bit表示协议版本号 (Version)。IM中,QQ使用的时二进制协议。

二进制协议有几个特点:

1.3、流式XML

Xmpp协议就是使用流式XML,像Gtalk,校内通都是基于Xmpp的。

举一个罗密欧给朱丽叶发消息的例子:

< message to='romeo@example.net' from='juliet@example.com' type='chat' xml:lang='en' >
<body>Wherefore art thou, Romeo?</body>
</message>

Xmpp用 romeo@example.net 来标示一个账号,称为JID 。JID由两个部分组成 ,前半部分是该域中的账号体系 ,后半部分是域标识(编者注:JID还包含一个Resource部分)。Xmpp协议可以实现跨域的互通。例如Gtalk和校内通用户聊天。只要服务端实现了s2s服务(server to server) ,不过现在的IM基本没有互通需求 ,所以 这个服务基本没有人实现。

Xmpp协议有几个特点:

我个人强烈不建议使用Xmpp,特别是无线端IM,这个是google上个世纪的产物了,如果要用,一定要自己做压缩 ,减少网络流量。

实际例子

下面来看一个IM协议的实际例子 ,一般常见的做法是:定长二进制包头,可扩展变长包体可以使用用文本、XML等。 百度Hi 的IM包体用的是XML ,58同城的IM包体用的是protobuffer,包头负责传输和解析效率,包体与业务无关负责保证扩展性。

这是一个简单例子,我们来看一下包头和包体 。

定长包头(16个字节)

变长包体

可选择 Xml、protobuffer、mcpack等,某些公司使用protobuffer,作者也强烈推荐,主要有几个原因:

下面贴一个protobuffer写的用户登录协议的示例:

2、安全层

IM协议,消息的保密性非常重要 ,谁都不希望自己聊天内容被看到

1、HTTPS

稍微复杂,代价有点高 。

2、自行加解密

密钥管理方式有多种

1、固定密钥
服务端和客户端约定好一个密钥,同时约定好一个加密算法(eg:AES ),每次客户端IM在发送前,就用约定好的算法,以及约定好的密钥加密 再传输 ,服务端收到报文后,用约定好的算法,约定好的密钥再解密 。这种方式,密钥和算法 对程序员都是透明的 。有些公司cookie就是这么使用的。

2、一人一密钥
简单说来就是每个人的密钥是固定的,但是每个人之间又不同,其实就是在固定密钥的算法中包含用户的某一特殊属性,比如用户uid、手机号等。

据传,QQ是这么使用这种方式(未核实)

3、动态密钥

大家了解ssl的过程么,动态密钥 一Session一密钥的安全性更高,每次传输交互前协商密钥, 客户端第一个报文:服务端,请告诉我这次通话的密钥 ,服务器就随机生成一个,返回给客户端 然后这次会话用这个密钥来通信 ,这样可以简单的做到动态密钥,但是一旦攻击者截取报文,就能知道你的动态密钥。

SSL的用法,是2次生成非对称加密密钥,1次生成对称加密密钥 ,具体详情这里不展开,有兴趣的可以深入研究一下。

3、传输层

传输层:TCP/UDP

现在的IM传输层基本都是使用TCP。有了epoll/kqueue等技术后 ,单机多连接就不是瓶颈了,单机几十万链接没什么问题。58同城现在线上单机连接好像是10w?(可能单机性能测试可以到百万,线上一般跑到几十万)

关于QQ是使用UDP问题(作者观点)

10多年前,一台服务器支撑不了1W个TCP连接 ,腾讯的同时在线量高,没办法,只有用UDP了 ,但UDP又不可靠,腾讯使用UDP实现了TCP的超时/重传/确认等机制

三、WEB聊天室

1、需求

下面是一个Web聊天室的截图

Web聊天室需求主要有一下几点: 1)用户可以设置自己的名字 2)进入聊天室后,可以看到所有其他人的名字 3)可以看到所有人的聊天 4)可以发言给所有人

上面是Web聊天室的一些简单业务(像群吧?) ,设置名字/拉取其他人的信息 都是简单的请求响应式的需求这里不展开,主要重点讲怎么看历史聊天消息,怎么发消息给别人 。

2、系统架构

Web站点三层架构大家都很熟悉,大致是这个样子

LAMP是个典型解决方案 ,更典型的是这个样子

对于聊天室的简单需求,一个存用户信息,一个存消息两个表就可以了,下面是一个简化了的示例:

user( name vchar(16) unique ); message( time timestamp, name vchar(16), msg vchar(140) );

有人进入,就往user里插入 ,有人退出,就从user里删除 ,有人发消息,就往message里插入 ,有人进入,往message里拉取,就能查询历史信息。

3、技术核心点

Web聊天室的核心店在于如何把发送的消息通知User表里的所有人?消息实时性,主要有三种方式,websocket、flashsocket、http轮询,本文只讲http轮询。

1、轮询

什么是轮询?
举个例子,在火车上想上洗手间,挤到洗手间旁,却发现洗手间有人,于是你只能回座位继续等。过了N分钟,又朝洗手间的方向挤过去,却发现洗手间还是有人,又只能回坐等。这么一而再,再而三的每隔N分钟去洗手间查看洗手间是否有蹲位,这就是轮询。

程序代码:while(1){sleep 500ms; get msg;}

大部分人最容易想到的解决方案就是轮询(poll) 。十几年前,四通利方/碧海银沙就是用的轮询,轮询拉取消息,每隔几秒往message表里,拉取最新的聊天室消息 ,这样做能简单实现一个聊天室。

但轮询问题也显而易见 ,每隔N分钟,轮询调用 “获取消息”接口,有可能出现消息的延时,某一时刻刚刚拉取完消息,突然又产生了一条新消息,这条消息就必须等到N分钟之后,再次发起“获取消息”轮询时,才有机会获取到。 可以降低时间间隔来降低延时,但绝大部分的轮询调用,都没有消息返回,造成服务端极大的资源浪费

2、消息连接

Web聊天室通过“http消息连接”来保证消息的绝对实时性,何谓消息连接?

用户和服务器建立一条http连接,专门用来传递Notify 。例如,手机上,web聊天室里,有一个B用户 专门有一条http消息连接,用来投递Notify(消息),如下图 。

消息连接如何保证 web聊天室消息的实时性呢?它具有以下几个特性:

1、没有消息到达的时候,这个http消息连接将被夯住,不返回,由于http是短连接,这个http消息连接最多被夯住90秒,就会被断开(这是浏览器或者webserver的行为)。

2、如果http消息连接被断开,立马再发起一个http消息连接

如上图,http消息连接90s超时了,服务器返回空了,web浏览器会立马再次发起一个新的消息连接 。目的是,保证一个用户一直有一条消息连接连着,可以接受消息

3、每次收到消息时,这个消息连接就能及时将消息带回浏览器页面,并且在返回后,会立马再发起一个http消息连接

如上图,某人发送了一条聊天室消息 ,这个消息要投递给user里的所有人,B是其中之一,此时会有一条消息连接在,消息连接就直接将消息带回 ,带会消息后,立马再发起消息连接 。

4、消息池

上面三大特性保证了:a)任何时间都有消息连接在 ,b)消息能在第一时间通过消息连接返回

但这里有个小概率事件,正在返回消息的时候(可以认为此时没有消息连接),瞬间又到达了一条消息怎么办,此时服务端要有一个类似于“消息池”的东西,将这个消息暂存起来 。消息连接到达后,从消息池中将消息取回,再通过消息连接返回。


看上图中的步骤1-7

1)消息要投递给聊天室中的所有人,B是其中之一 2)此时没有消息连接,msg放入消息池 3)消息连接来晚了 4)从消息池中获取消息 5)获取到消息了 6)返回消息给web浏览器里的B 7)新的消息连接发起

结论:

Web聊天室通过http长轮询可以保证消息的实时性。这种实时性的保证不是通过增加轮询频率来保证的,而是通过夯住http消息连接来保证的,在大部分时间没有实时消息的情况下,这个http消息连接对于webserver的请求压力是90秒1次,能够大大节省了web服务器资源。

这个消息连接的思想,是一个观察者模式 ,所有的聊天室用户是observer,聊天室是subject,observer订阅subject ,subject保存有所有observer的集合,当有消息发出,即subject发生改变时,通过http消息连接反向通知subject

这里重点讨论了 web聊天室 消息的实时性,聊天消息的可靠投递,暂时也先不展开了

即时,是相对时间而不是绝对时间 ,m的即时,消息投递在几百毫秒,1秒内投递完成,一般都可以接受(站点应用这个时间就接受不了解),这个时间对未来我讲im服务的网络模型,影响很大 。 im的所有业务逻辑,都是在这几百毫秒内完成的 。

4、IM典型业务场景

IM业务逻辑有一定的复杂度,举例IM中一个典型业务场景,用户A将用户B加入到分组G中,如下图:

这里包含多少业务逻辑呢 第一步,判断A是否是正常im用户
第二步,判断B是否是正常im用户
第三步,判断分组G是否存在
第四步,是否A和B已经是好友
第五步,用户A是否加黑了B
第六步,用户B是否加黑了A
第七步/用户A的加好友频率是否过快
第八步,加好友的验证文字是否合法。“我是xx,请加我为好友”就是这个文字的验证。
第九步,检查B的加好友策略 ,是“允许所有”还是“需要验证”还是“禁止所有”

都9步了,真正的加好友步骤却还没开始,这里的每一个步骤,都不是一个简单的本地cpu计算能完成的,都需要访问数据库或者后端服务 ,所以im的业务逻辑是很复杂的,做过的人懂。

上面有一个步骤,是检查加好友的验证短语的合法性 。IM系统中,所有能被别人看到的话,都要经过antispam验证 ,antispam一般有敏感词(政治,黄色)过滤,消息频率过滤,广告过滤 ,每一个步骤,都有很复杂的词库过滤,或者分析过滤(分析一句话是不是广告,非常复杂)

四、Q&A

Q1: xmpp中,在跨域通讯网络没有保证的情况下,xmpp如何保证跨域消息的可靠送达的。

A1:im消息的可达性,是通过超时重传确认保证的(未来会再次重点讲细节),跨域只是提供了一种不同域的通信机制

Q2:应用层协议设计中,为啥 version 会放在 magic_num 之前呢? 为啥magic_num 不是第一个呢? 是考虑不同版本的 magic_num 不一样?

A2:是的

Q3:我没懂magic_num的作用,这个是常量吗?另外,为啥不直接采用http,而要自定义协议?

A3:web上可以选择http作为应用层协议,直接tcp长连接搞的话协议得自己来

Q4:聊天室怎么保证消息的可靠性?

A4: 可靠性以后讲,或者可以看下刚才群内有同学发的那篇文章 webim如何保证消息的可靠投递

Q5:安全层加密, 是对应用层的:定长二进制包头 + 可扩展变长包体 都做加密?

A5:包体和业务相关,要加密

Q6: 现在通信行业的IM(RCS等业务)基本都用SIP(单独或结合MSRP)来做,协议安全用TCP/TLS,WebRTC也用SIP,请问@58沈剑 不用这些公共协议而用私有协议的主要出发点除了性能,还有什么?

A6:自己做更可控,结合自己业务做优化。另,im行业准标准协议是xmpp。

Q7:消息推送的话,用长连接保持的话,现在也比较通用了吧?

A7:能用tcp就用tcp,有些场景,例如web,选择http消息连接是被逼的

Q8:实时推送,http轮询和websocket如何选择呢?

A8:websocket有兼容性问题,很多浏览器不支持,据我了解,大规模高在线的im,好像web端还木有用websocket搞的

Q9:手机端的聊天室如何处理用户频繁断线的问题?如何在断线后获取服务器端聊天历史并与本地聊天历史合并?

A9:web聊天室,消息连接不上,消息就放在消息池里,上来再给他,一定时间上不来,就丢掉。消息id可以去重。

Q10:能不能介绍一下注册/IM/Subscribe/Presence等服务器结合起来的部署架构?

A10:架构后续课程分享

Q11:websocket有什么弊端?目前单机最高业务承载多少?

A11:同A8回答。

Q12:移动端im的注意事项

A12:后续专门讲移动优化

Q13:聊天室达到10万级别的时候,在效率推送上是怎么考虑的?

A13:聊天室和群一样,消息扩散系数很影响性能,一般群不能到这个级别,这个级别的群对服务器影响很大(微信群上限是多少?qq群上限是多少?)

Q14:我想问下,刚说的消息池是全局的还是针对每个用户单独的?

A14:消息池本质是个map,key是uid

Q15:为什么不用BOSH(Bidirectional-streams Over Synchronous HTTP)来描述刚才web HTTP机制?有什么细微区别吗?

A15:我猜bosh的本质也是消息连接,bosh/comet还是什么,叫法不一样,实现方式应该是类似的

Q16:另外你说的移动IM不建议XMPP协议,是单指XML格式本身的缺陷,有效数据太少,数据解析慢,仅仅是这个原因吗?解析这方面有做过具体的性能测试吗?另外如果是有效数据太少,是否可以稍微扩展就可以大幅度减少无效数据问题。还是有其它方面原因,比如XMPP协议本身交互协商次数太多的问题?

A16:xmpp解析慢,有效传输率低(无线端的表现就是耗流量),你们猜用xmpp发一个登录包多大?

Q17:websocket性能会比long-polling好多少呢?

A17:tcp长连接 和 http短连接 的差异

Q18:聊天消息中含html、css、js相关代码是如何处理的?

A18:这是一个富文本消息的好问题,服务器不管消息的内容,只进行投递,富文本消息的内容,是客户端需要理解的

Q19:im怎么保证时序性?当服务器接收msgpack时处理不过来,客户端就会阻塞发送消息。但没阻塞接收的消息。这样发送与接着就不同步了

A19:一言难尽,简单说,单对单的消息,用uid+msgid进行时序保证;群消息,用gid+msgid进行时序保证。

官方出版

QCon高可用架构群官方博客 +