即时通讯IM技术领域基础篇_VOIP_SIP_OpenFire_doubango

时间:2020-10-20

视频聊天软件


即时通讯IM技术领域提高篇

准备工作(协议选型)

xxx项目架构

IM 关键技术点 & 策略机制

典型IM业务场景

存储结构简析

udp协议虽然实时性更好,但是如何处理安全可靠的传输并且处理不同客户端之间的消息交互是个难题,实现起来过于复杂. 目前大部分IM架构都不采用UDP来实现.

但是为啥还需要HTTP呢?

核心的TCP长连接,用来实时收发消息,其他资源请求不占用此连接,保证实时性

http可以用来实现状态协议(可以用php开发)

IM进行图片/语言/大涂鸦聊天的时候: http能够很方便的处理 断点续传和分片上传等功能.

TCP: 维护长连接,保证消息的实时性, 对应数据传输协议.

IM协议选择原则一般是:易于拓展,方便覆盖各种业务逻辑,同时又比较节约流量。节约流量这一点的需求在移动端IM上尤其重要 !!!

xmpp: 协议开源,可拓展性强,在各个端(包括服务器)有各种语言的实现,开发者接入方便。但是缺点也是不少:XML表现力弱,有太多冗余信息,流量大,实际使用时有大量天坑。

MQTT: 协议简单,流量少,但是它并不是一个专门为IM设计的协议,多使用于推送. 需要自己在业务上实现群,好友相关等等(目前公司有用MQTT实现通用IM框架).

SIP: 多用于VOIP相关的模块,是一种文本协议. sip信令控制比较复杂

私有协议: 自己实现协议.大部分主流IM APP都是是使用私有协议,一个被良好设计的私有协议一般有如下优点:高效,节约流量(一般使用二进制协议),安全性高,难以破解。 xxx项目基本属于私有定制协议<参考了蘑菇街开源的TeamTalk>, 后期通用IM架构使用MQTT

协议设计的考量:

网络数据大小 —— 占用带宽,传输效率:虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输,必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络IO,提高传输效率;

网络数据安全性 —— 敏感数据的网络安全:对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密(xxx项目目前提供C++的加密库给客户端使用)

编码复杂度 —— 序列化和反序列化复杂度,效率,数据结构的可扩展性

协议通用性 —— 大众规范:数据类型必须是跨平台,数据格式是通用的

提供序列化和反序列化库的开源协议: pb,Thrift. 扩展相当方便,序列化和反序列化方便(xxx项目目前使用pb)

文本化协议: xml,json. 序列化,反序列化容易,但是占用体积大(一般http接口采用json格式).

...

...

同时支持TCP 和 HTTP 方式, 关联性不大的业务服务独立开来

服务支持平行扩展,平行扩展方便且对用户无感知

cache db层的封装,业务调用方直接调用接口即可.

除了Access server是有状态的,其他服务无状态

各个服务之间,通过rpc通信,可以跨机器.

oracle里面都是模块化,有点类似MVC模式, 代码解耦, 功能解耦.

缺点

改进

push server 没有业务,仅仅是转发Access和oracle之间的请求

缺点

改进

Access server和用户紧密连接,维持长连接的同时,还有部分业务

缺点

改进:

为什么有可能会乱序?

对于在线消息, 一发一收,正常情况当然不会有问题

对于离线消息, 可能有很多条.

怎么保证不乱序?

每条消息到服务端后,都会生成一个全局唯一的msgid, 这个msgid一定都是递增增长的(msgid的生成会有同步机制保证并发时的唯一性)

针对每条消息,会有消息的生成时间,精确到毫秒

拉取多条消息的时候,取出数据后,再根据msgid的大小进行排序即可.

消息为什么可能会重复呢?

这种情况下,就可能会需要有重发机制. 客户端和服务端都可能需要有这种机制.

既然有重复机制,就有可能收到的消息是重复的.

怎么解决呢? 保证不重复最好是客户端和服务端相关处理

消息meta结构里面增加一个字段isResend. 客户端重复发送的时候置位此字段,标识这个是重复的,服务端用来后续判断

服务端为每个用户缓存一批最近的msgids(所谓的localMsgId),如缓存50条

服务端收到消息后, 通过判断isResend和此msgid是否在localMsgId list中. 如果重复发送,则服务端不做后续处理.

因为仅仅靠isResend不能够准备判断,因为可能客户端确实resend,但是服务端确实就是没有收到......

最简单的就是服务端每传递一条消息到接收方都需要一个ack来确保可达

服务端返回给客户端的数据,有可能客户端没有收到,或者客户端收到了没有回应.

考虑一个账号在不同终端登录后的情况.

这里提供两种方案供参考(本质思想一样,实现方式不同)

每个用户的每条消息都一定会分配一个唯一的msgid

服务端会存储每个用户的msgid 列表

客户端存储已经收到的最大msgid


image.png

优点:

根据服务器和手机端之间sequence的差异,可以很轻松的实现增量下发手机端未收取下去的消息

对于在弱网络环境差的情况,丢包情况发生概率是比较高的,此时经常会出现服务器的回包不能到达手机端的现象。由于手机端只会在确切的收取到消息后才会更新本地的sequence,所以即使服务器的回包丢了,手机端等待超时后重新拿旧的sequence上服务器收取消息,同样是可以正确的收取未下发的消息。

由于手机端存储的sequence是确认收到消息的最大sequence,所以对于手机端每次到服务器来收取消息也可以认为是对上一次收取消息的确认。一个帐号在多个手机端轮流登录的情况下,只要服务器存储手机端已确认的sequence,那就可以简单的实现已确认下发的消息不会重复下发,不同手机端之间轮流登录不会收到其他手机端已经收取到的消息。

内网即时通讯工具

image.png

假如手机A拿Seq_cli = 100 上服务器收取消息,此时服务器的Seq_svr = 150,那手机A可以将sequence为[101 - 150]的消息收取下去,同时手机A会将本地的Seq_cli  置为150

image.png


手机A在下一次再次上来服务器收取消息,此时Seq_cli = 150,服务器的  Seq_svr = 200,那手机A可以将sequence为[151 - 200]的消息收取下去.

image.png


假如原手机A用户换到手机B登录,并使用Seq_cli = 120上服务器收取消息,由于服务器已经确认sequence <= 150的消息已经被手机收取下去了,故不会再返回sequence为[121 - 150]的消息给手机B,而是将sequence为[151 - 200]的消息下发给手机B。

每个用户的每条消息都一定会分配一个唯一的msgid

服务端会存储每个用户的msgid 列表

客户端存储已经收到的最大msgid

image.png

这两种方式的优缺点?

方式二中,确认机制都是多一次http请求. 但是能够保证及时淘汰数据

方式一中,确认机制是等到下一次拉取数据的时候进行确定, 不额外增加请求, 但是淘汰数据不及时.

心跳功能: 维护TCP长连接,保证长连接稳定性, 对于移动网络, 仅仅只有这个功能吗?

运营商通过NAT(network adddress translation)来转换移动内网ip和外网ip,从而最终实现连上Internet,其中GGSN(gateway GPRS support Node)模块就是来实现NAT的过程,但是大部分运营商为了减少网关NAT的映射表的负荷,若一个链路有一段时间没有通信就会删除其对应表,造成链路中断,因此运营商采取的是刻意缩短空闲连接的释放超时,来节省信道资源,但是这种刻意释放的行为就可能会导致我们的连接被动断开(xxx项目之前心跳有被运营商断开连接的情况,后面改进了心跳策略,后续还将继续改进心跳策略)

NAT方案说白了就是将过去每个宽带用户独立分配公网IP的方式改为分配内网IP给每个用户,运营商再对接入的用户统一部署NAT设备,NAT的作用就是将用户网络连接发起的内网IP,以端口连接的形式翻译成公网IP,再对外网资源进行连接。

从mobile 到GGSN都是一个内网,然后在GGSN上做地址转换NAT/PAT,转换成GGSN公网地址池的地址,所以你的手机在Internet 上呈现的地址就是这个地址池的公网地址

心跳保证客户端和服务端的连接保活功能,服务端以此来判断客户端是否还在线

心跳还需要维持移动网络的GGSN

最常见的就是每隔固定时间(如4分半)发送心跳,但是这样不够智能.

4分半的原因就是综合了各家移动运营商的NAT超时时间

心跳时间太短,消耗流量/电量,增加服务器压力.

心跳时间太长,可能会被因为运营商的策略淘汰NAT表中的对应项而被动断开连接

智能心跳策略

为了保证收消息及时性的体验,当app处于前台活跃状态时,使用固定心跳。

app进入后台(或者前台关屏)时,先用几次最小心跳维持长链接。然后进入后台自适应心跳计算。这样做的目的是尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响。

维护移动网GGSN(网关GPRS支持节点)

参考微信的一套自适应心跳算法:

精简心跳包,保证一个心跳包大小在10字节之内, 根据APP前后台状态调整心跳包间隔 (主要是安卓)

掉线后,根据不同的状态需要选择不同的重连间隔。如果是本地网络出错,并不需要定时去重连,这时只需要监听网络状态,等到网络恢复后重连即可。如果网络变化非常频繁,特别是 App 处在后台运行时,对于重连也可以加上一定的频率控制,在保证一定消息实时性的同时,避免造成过多的电量消耗。

断线重连的最短间隔时间按单位秒(s)以4、8、16...(最大不超过30)数列执行,以避免频繁的断线重连,从而减轻服务器负担。当服务端收到正确的包时,此策略重置

有网络但连接失败的情况下,按单位秒(s)以间隔时间为2、2、4、4、8、8、16、16...(最大不超过120)的数列不断重试

未读消息索引存在的意义在于保证消息的可靠性以及作为离线用户获取未读消息列表的一个索引结构。

未读消息索引由两部分构成,都存在redis中:

假设A有三个好友B,C,D。A离线。B给A发了1条消息,C给A发了2条消息,D给A发了3条消息,那么此时A的未读索引结构为:

hash结构

zset结构

消息上行以及队列更新未读消息索引是指,hash结构对应的field加1,然后将消息id追加到相应好友的zset结构中。

接收ack维护未读消息索引则相反,hash结构对应的field减1,然后将消息id从相应好友中的zset结构中删除。

该流程用户在离线状态的未读消息获取。

该流程主要由sessions/recent接口提供服务。流程如下:

和在线的流程相同,离线客户端读取了未读消息后也要发送接收ack到业务端,告诉它未读消息已经下发成功,业务端负责维护该用户的未读消息索引。

和在线流程不同的是,这个接收ack是通过调用messages/lastAccessedId接口来实现的。客户端需要传一个hash结构到服务端,key为通过sessions/recent接口下发的好友id,value为sessions/recent接口的未读消息列表中对应好友的最大一条消息id。

服务端收到这个hash结构后,遍历它

这样就完成了离线流程中未读消息索引的维护。

如果消息标记为offline,则将消息入库,写缓存(只有离线消息才写缓存),更新未读消息索引,然后调用apns进行推送。

如果消息标记为online,则直接将消息入库即可,因为B已经收到这条消息。

如果消息标记为redeliver,则将消息写入缓存,然后调用apns进行推送。

拆分出来的目的:

真的能够起到这样的效果么?

拆分出来的connd server 还是有可能会需要重启的, 这时候怎么办呢 ?关键性问题还是没有解决

加一层服务,是打算通过共享内存的方式,connd 只管理连接。access 更新升级的时候,用户不会掉线。

目前Access服务不重, 拆分出来真有必要吗?

真要拆分, 那也不是这么拆分, 是在Oracle上做拆分, 类似微服务的那种概念

稳定性不是这么体现,原来 connd 的设计,更薄不承担业务,而现在的 access 还是有一些业务逻辑,那么它升级的可能性就比较高。

access 拆分,目的就是让保持连接的那一层足够薄,薄到怎么改业务,它都不用升级代码(tcp 不会断)。

连接层更稳定   - - -  需要有硬性指标来判断才能确定更稳定,因为Access的服务不重,目前也不是瓶颈点.

减少重启,方便Access服务升级 - - - 不能通过增加一层服务来实现重启升级,需要有其他机制来确保服务端进行升级而不影响TCP长连接上的用户

增加一个服务,就多了一条链路, 就可能会导致服务链路过长,请求经过更多的服务,会导致服务更加不可用. 因为要保证每个服务的可用性都到99.999%(5个9)是很难的,增加一个服务,就会降低整个服务的可用性.

架构改进一定要有数据支撑, 要确实起到效果, 要有数据输出才能证明这个改进是有效果的,要不然花了二个月时间做改进,结果没有用,浪费人力和时间,还降低开发效率

方案: 增加一条信令交互,服务端如果要重启/缩容, 告知连接在此Access上的所有客户端,服务端要升级了,客户端需要重连其他节点

等确定当前Access节点上的所有客户端都连接到其他节点后, 当前Access节点再进行重启/下线/缩容.

怎么扩容? 如果需要扩容,则增加新的节点后,通过etcd进行服务发现注册.客户端通过router server请求数据后,拉取到相关节点.

如果当前3个节点扛不住了,增加2个节点, 这个时候,要能够马上缓解当前3个节点压力,需要怎么做?

服务端发送命令给当前节点上的客户端,让客户端连接到新增节点上.

服务端还需要确定是否有部分连接到其他节点了,然后再有相应的策略.

按照之前的方式,客户端重新登录请求router server,然后再进行连接的话,这是不能够马上缓解压力的,因为新增的节点后, 当前压力还是在之前几个节点

所以, 服务端需要有更好的机制,来由服务端控制

线上机器都有防火墙策略(包括硬件防火墙/软件防火墙)

硬件防火墙: 硬件防火墙设备,很贵,目前有采购,但是用的少

软件防火墙: 软件层面上的如iptable, 设置iptable的防火墙策略

TCP 通道层面上

要能够发送消息, 必须要先登录

要登录, 必须有token,有秘钥

收发消息也可以设置频率控制

socket建连速度的频率控制, 不能让别人一直建立socket连接,要不然socket很容易就爆满了,撑不住了

收发消息频率控制, 不能让别人一直能够发送消息,要不然整个服务就挂掉了

为啥xmpp不适合,仅仅是因为xml数据量大吗 ?

目前也有方案是针对xmpp进行优化处理的. 因此流量大并不是主要缺点

还有一点就是消息不可靠,它的请求及应答机制也是主要为稳定长连网络环境所设计,对于带宽偏窄及长连不稳定的移动网络并不是特别优化

因此设计成支持多终端状态的XMPP在移动领域并不是擅长之地

为啥mqtt不适合? 为啥xxx项目没有用mqtt ?

mqtt 适合推送,不适合IM, 需要业务层面上额外多做处理, 目前已经开始再用

xxx项目不用mqtt是历史遗留问题,因为刚开始要迅速开展,迅速搭建架构实现,因此用来蘑菇街的teamtalk.

如果后续选型的话, 如果没有历史遗留问题,那么就会选择使用mqtt

除了数据量大, 还要考虑协议的复杂度, 客户端和服务端处理协议的复杂度?

协议要考虑容易扩展, 方便后续新增字段,  支持多平台

要考虑客户端和服务端的实现是否简单

编解码的效率

服务需要能够跨机房,尤其是有状态的节点.

需要储备多机房容灾,防止整个机房挂掉.

维持TCP长连接,包括心跳/超时检测

收包解包

防攻击机制

等待接收消息回应(这个之前没有说到,就是把消息发送给接收方后还需要接收方回应)

如何保证消息不丢,不重? 怎么设计消息防丢失机制?

对于长连接, 怎管理这些长连接?