即时通信IM技术领域基础篇

[TOC]php

即时通信IM技术领域基础篇

即时通信IM技术领域提升篇ios

议题

  1. 准备工做(协议选型)redis

    • 网络传输协议选择 和 数据通讯协议选择
  2. xxx项目架构算法

    • 架构优缺点
    • 架构改进之路
  3. IM 关键技术点 & 策略机制数据库

    • 如何保证消息不丢/不乱序/不重复
    • 心跳策略
    • 重连策略
  4. 典型IM业务场景json

    • 用户A发送消息给用户B
    • 用户A发送消息到群C
  5. 存储结构简析后端

准备工做(协议选型)

选用什么网络传输协议(TCP/UDP/HTTP) ?

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

  2. 可是为啥还须要HTTP呢?安全

    • 核心的TCP长链接,用来实时收发消息,其余资源请求不占用此链接,保证明时性服务器

    • http能够用来实现状态协议(能够用php开发)

      • 朋友圈
      • 用户我的信息(好友信息,帐号,搜索等..)
      • 离线消息用拉模式,避免 tcp 通道压力过大,影响即时消息下发效率
      • 等等...
    • IM进行图片/语言/大涂鸦聊天的时候: http可以很方便的处理 断点续传和分片上传等功能.

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

    • 目的: 及时收发消息

选用什么数据通讯协议?

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

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

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

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

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

  2. 协议设计的考量:

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

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

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

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

  3. 经常使用序列化协议

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

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

xxx项目系统架构

前期架构

...

改进后架构

...

架构的优缺点

优势

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

    • php server
    • router server
    • user center
    • Access server
    • oracle server
  2. 服务支持平行扩展,平行扩展方便且对用户无感知

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

  4. 除了Access server是有状态的,其余服务无状态

  5. 各个服务之间,经过rpc通讯,能够跨机器.

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

缺点

  1. oracle 太过庞大, 能够把某些业务抽取出来

    • 缺点

      • 业务太庞大,多人开发不方便,容易引发code冲突
      • 若是某个小功能有异常,可能致使整个服务不可用
    • 改进

      • oracle里面耦合了apns server, 能够把apns 单独抽取出来. (xxx项目目前已经开始接入通用push推送系统了,相似把apns抽取出来).
  2. push server 没有业务,仅仅是转发Access和oracle之间的请求

    • 缺点

      • 须要单独维护一个比较鸡肋的服务,增长运维成本
    • 改进

      • 把push server合并到Access中,减小一层rpc调用中间环节.减小运维成本还能提升效率(xxx项目新架构已经把push server干掉融合到Access里面)
  3. Access server和用户紧密链接,维持长链接的同时,还有部分业务

    • 缺点

      • 维持着长链接,若是升级更新的话,势必会影响在线用户的链接状态
      • 偶尔部分业务,下降长链接的稳定性
    • 改进:

      • 把Access server 中维持长链接部分抽取出来一个connd server:
        • 仅仅维持长链接,收发包. 不耦合任何业务(xxx项目目前正在改进这个架构,还未上线)

IM 关键技术点

技术点一之: 如何保证消息可达(不丢)/惟一(不重复)/保序(不乱序)

最简单的保序(不乱序)

  1. 为何有可能会乱序?

    • 对于在线消息, 一发一收,正常状况固然不会有问题

      • 可是,若是收到消息的时候,忽然网络异常了,收不到消息了呢?
        • 服务端就会重发或者转离线存储(xxx项目的机制当即转离线存储)
    • 对于离线消息, 可能有不少条.

      • 拉取的时候,通常会把离线的消息都一次性的拉取过来
        • 多条消息的时候,就要保证收取到的消息的顺序性.
  2. 怎么保证不乱序?

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

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

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

保证惟一性(不重复)

  1. 消息为何可能会重复呢?

    • 移动网络的不稳定性,可能致使某天消息发送不出去,或者发送出去了,回应ack没有收到.
      • 这种状况下,就可能会须要有重发机制. 客户端和服务端均可能须要有这种机制.

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

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

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

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

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

    • 由于仅仅靠isResend不可以准备判断,由于可能客户端确实resend,可是服务端确实就是没有收到......

保证可达(不丢且不重)

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

    • 可是ack也有可能在弱网环境下丢失.
  2. 服务端返回给客户端的数据,有可能客户端没有收到,或者客户端收到了没有回应.

    • 所以,就必定要有完善的确认机制来告知客户端确实收到了. 有且仅有一次.
  3. 考虑一个帐号在不一样终端登陆后的状况.

    • 消息要可以发送到当前登陆的终端,并且又不能重复发送或者拉取以前已经拉取过的数据.

技术点二之: msgID机制

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

序列号msgid机制 & msgid确认机制(方案一):

  • 每一个用户的每条消息都必定会分配一个惟一的msgid

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

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

image.png

优势:

  1. 根据服务器和手机端之间sequence的差别,能够很轻松的实现增量下发手机端未收取下去的消息

  2. 对于在弱网络环境差的状况,丢包状况发生几率是比较高的,此时常常会出现服务器的回包不能到达手机端的现象。因为手机端只会在确切的收取到消息后才会更新本地的sequence,因此即便服务器的回包丢了,手机端等待超时后从新拿旧的sequence上服务器收取消息,一样是能够正确的收取未下发的消息。

  3. 因为手机端存储的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确认机制(方案二: xxx项目目前方案):

  • 每一个用户的每条消息都必定会分配一个惟一的msgid

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

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

    • 对于单聊,群聊,匿名分别存储(某人对应的id,某群对应的id).

image.png

思考

这两种方式的优缺点?

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

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

技术点三之: 心跳策略

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

  1. 心跳其实有两个做用

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

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

      • 运营商经过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 上呈现的地址就是这个地址池的公网地址

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

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

    • 心跳时间过短,消耗流量/电量,增长服务器压力.

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

  3. 智能心跳策略

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

      • 大部分移动无线网络运营商都在链路一段时间没有数据通信时,会淘汰 NAT 表中的对应项,形成链路中断。NAT超时是影响TCP链接寿命的一个重要因素(尤为是国内),因此客户端自动测算NAT超时时间,来动态调整心跳间隔,是一个很重要的优化点。
    • 参考微信的一套自适应心跳算法:

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

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

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

技术点四之: 断线重连策略

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

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

  2. 有网络但链接失败的状况下,按单位秒(s)以间隔时间为二、二、四、四、八、八、1六、16...(最大不超过120)的数列不断重试

  3. 为了防止雪崩效应的出现,咱们在检测到socket失效(服务器异常),并非立马进行重连,而是让客户端随机Sleep一段时间(或者上述其余策略)再去链接服务端,这样就可使不一样的客户端在服务端重启的时候不会同时去链接,从而形成雪崩效应。

典型IM业务场景流程

  1. 用户A发送消息给用户B

    • A 经过帐号密码获取token.
    • A 拿着token进行login
    • 服务端缓存用户信息并维持登陆状态
    • A 打包数据发送给服务端
    • 服务端检测A用户是否风险用户
    • 服务端对消息进行敏感词检查(这个重要)
    • 服务端生成msgid
    • 服务端进行好友检测(A/B)
    • 服务端进行重复发送检测
    • 服务端获取B的链接信息,并判断在线状态
    • 若是在线,直接发送给B,并入cache和db
    • 若是不在线,直接存储.若是是ios,则进行apns.
    • 在线的B,收到消息后回应ack进行确认.
  2. 用户A发送消息到群C

存储结构

未读索引列表

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

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

    • 记录用户每一个好友的未读数的hash结构
    • 每一个好友对应一个zset结构,里面存着全部未读消息的id。
  • 假设A有三个好友B,C,D。A离线。B给A发了1条消息,C给A发了2条消息,D给A发了3条消息,那么此时A的未读索引结构为:

  • hash结构

    • B-1
    • C-2
    • D-3
  • zset结构

User MsgId 1 MsgId 2 MsgId 3
B 1 - -
C 4 7 -
D 8 9 10
  • 消息上行以及队列更新未读消息索引是指,hash结构对应的field加1,而后将消息id追加到相应好友的zset结构中。

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

消息下行(未读消息的获取)

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

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

  • hgetall读取未读消息索引中的hash结构。
  • 遍历hash结构,若未读数不为0,则读取相应好友的zset结构,取出未读消息id列表。
  • 经过消息id列表到缓存(或穿透到数据库)读取消息内容,下发给客户端。

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

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

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

  • 清空相应缓存
  • 经过zremrangebyscore操做清空相应好友的zset结构
  • 将未读消息索引中的hash结构减掉zremrangebyscore的返回值

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

队列处理流程

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

  • 若是消息标记为online,则直接将消息入库便可,由于B已经收到这条消息。

  • 若是消息标记为redeliver,则将消息写入缓存,而后调用apns进行推送。

讨论后的疑问

把链接层Access拆一层connd server出来的考量和目的,到底有没有必要?

  1. 拆分出来的目的:

    • 链接层更稳定
    • 减小重启,方便Access服务升级
  2. 真的可以起到这样的效果么?

    • 链接层更稳定 - - - 须要有硬性指标来判断才能肯定更稳定,由于Access的服务不重,目前也不是瓶颈点.

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

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

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

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

    • 减小重启,方便Access服务升级 - - - 不能经过增长一层服务来实现重启升级,须要有其余机制来确保服务端进行升级而不影响TCP长链接上的用户

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

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

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

  3. 架构改进必定要有数据支撑, 要确实起到效果, 要有数据输出才能证实这个改进是有效果的,要否则花了二个月时间作改进,结果没有用,浪费人力和时间,还下降开发效率

    • 每一个阶段的架构可能都不同,根据当前阶段的用户量和热度来决定

怎么保证接入层服务重启升级? 服务扩/缩容?

  1. 方案: 增长一条信令交互,服务端若是要重启/缩容, 告知链接在此Access上的全部客户端,服务端要升级了,客户端须要重连其余节点

    • 这实际上是属于一种主动迁移的策略,这样客户端虽然仍是有重连,比咱们直接断链接会好一些.
  2. 等肯定当前Access节点上的全部客户端都链接到其余节点后, 当前Access节点再进行重启/下线/缩容.

  3. 怎么扩容? 若是须要扩容,则增长新的节点后,经过etcd进行服务发现注册.客户端经过router server请求数据后,拉取到相关节点.

  4. 若是当前3个节点扛不住了,增长2个节点, 这个时候,要可以立刻缓解当前3个节点压力,须要怎么作?

    • 按照以前的方式,客户端从新登陆请求router server,而后再进行链接的话,这是不可以立刻缓解压力的,由于新增的节点后, 当前压力仍是在以前几个节点

    • 因此, 服务端须要有更好的机制,来由服务端控制

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

      • 服务端还须要肯定是否有部分链接到其余节点了,而后再有相应的策略.

怎么防止攻击

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

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

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

  2. TCP 通道层面上

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

      • 目前设置的是独立ip建连速度超过100/s,则认为被攻击了,封禁此ip
    • 收发消息频率控制, 不能让别人一直可以发送消息,要否则整个服务就挂掉了

      • 要可以发送消息, 必需要先登陆

      • 要登陆, 必须有token,有秘钥

      • 收发消息也能够设置频率控制

目前市面上的开源/通用协议的比较选型

  1. 为啥xmpp不适合,仅仅是由于xml数据量大吗 ?

    • 目前也有方案是针对xmpp进行优化处理的. 所以流量大并非主要缺点

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

    • 所以设计成支持多终端状态的XMPP在移动领域并非擅长之地

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

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

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

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

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

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

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

    • 编解码的效率

跨机房, 多机房容灾

  1. 服务须要可以跨机房,尤为是有状态的节点.

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

刚讨论说到接入层有哪些功能的:

  1. 维持TCP长链接,包括心跳/超时检测

  2. 收包解包

  3. 防攻击机制

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

思考点(考核关键点)

  1. 消息为何可能会乱序? 怎么保证消息不乱序?

    • 考虑离线
    • 考虑网络异常
  2. 对于离线消息,存储方式/存储结构要怎么设计?

    • 考虑会有多我的发送消息
    • 考虑缓存+db的方式
  3. 如何保证消息不丢,不重? 怎么设计消息防丢失机制?

    • 考虑同一帐号可能会多终端登陆
    • 考虑弱网环境下,ACK也可能会丢失
  4. 对于长链接, 怎管理这些长链接?

    • 考虑快速查找
      • 后端数据来了, 怎么快速找到这个请求对应的链接呢
  5. 接入层节点有多个,并且是有状态的.经过什么机制保证从节点1下发的请求,其对应的响应仍是会回到节点1呢?

    • 或者说若是响应不回到节点1,而是回到节点2了会有什么弊端?

【"欢迎关注个人微信公众号:Linux 服务端系统研发,后面会大力经过微信公众号发送优质文章"】

个人微信公众号
相关文章
相关标签/搜索