一、IM技术概述

1、定义:

        即时通信Instant Messaging,简称IM)是一种透过网络进行实时通信的系统,允许两人或多人使用网络即时的传递文字消息文件、语音与视频交流。通常以网站、计算机软件或移动应用程序的方式提供服务。

2、技术要点:

1)、网络传输协议:

        IM系统传输即时消息无外乎使用UDP、TCP、基于TCP的HTTP这几种协议中的一种或几种

  • UDP协议实时性更好,但是如何处理安全可靠的传输并且处理不同客户端之间的消息交互是个难题,实现起来过于复杂;
  • HTTP协议属于扩展支持,数据传输量大;
  • TCP协议如果有海量用户的需求。如何保证单机服务器高并发量,如何做到灵活,扩展的架构。

2)、数据通信格式(协议)

  • 自定义二进制:信息体积小,占用带宽低,传输效率高,缺点是处理不同客户端之间的消息交互较复杂。
  • 提供序列化和反序列化库的开源协议:protocol buffers, json,  Thrift。是一种流行的通用数据格式,扩展相当方便,序列化和反序列化相当方便。
  • 文本化协议:序列化,反序列化容易(库支持),可视化强;缺点:相对于二进制存储占用体积大

3)、如何保证实时消息的可用性

  • IM中消息的投递中要保证发送方发送顺序与接收方展现顺序一致。
  • IM中如何保证在线实时消息的可靠投递,即消息的不丢失和不重复。
  • TCP 自带TCP Keepalive 无法满足需求。http://www.52im.net/thread-281-1-1.html

4)、如何处理群发消息以及离线消息

5)、如何进行消息持久化

二、goim的特点

1、多协议支持

        支持TCP、WebSocket。

2、可拓扑的架构

        Comet、Logic支持可动态无限扩展,只需要热更新配置信息。

        Comet模块可以扩展从而支持更多的连接数,Logic模块可以扩展去提供服务支持。Comet扩容以后,Job模块可以热重启。

3、基于Kafka做异步消息推送

        1)、Apache Kafka是一个分布式的发布-订阅消息系统,能够支撑海量数据的数据传递。

        2)、在离线和实时的消息处理业务系统中,Kafka都有广泛的应用。

        3)、Kafka将消息持久化到磁盘中,并对消息创建了备份保证了数据的安全(以时间复杂度为O(1)的方式提供消息持久化能力)。

        4)、Kafka在保证了较高的处理速度的同时,又能保证数据处理的低延迟和数据的零丢失。

        5)、Kafka delivery guarantee

        有这么几种可能的delivery guarantee:

  • At most once   消息可能会丢,但绝不会重复传输
  • At least one     消息绝不会丢,但可能会重复传输
  • Exactly once   每条消息肯定会被传输一次且仅传输一次。

 

4、使用自定义的数据格式

三、GoIM的架构设计

1、总体设计

2、Comet模块:用户代理服务器,用于客户端的连接

1)、客户端首先连接到comet服务,comet会RPC调用logic来校验用户的合法性

2)、logic会返回一个subKeycomet,该subKey成为该连接的唯一标示

3)、客户端接下来可以发心跳包给comet,客户端发送消息,与此同时,job服务将MQ-Kafka的消息转发到对应cometcomet再将其转发到对应的客户端

3、Logic模块:主要业务处理模块

1)、处理Comet对Logic的远程RPC调用的处理,如用户登录注册等。这些用户相关的请求会RPC调用Router模块,记录用户的UID、subKey、RoomID等。

2)、启用HTTP服务,监听comet上客户端的HTTP的消息,将这些聊天相关的业务发生到Kafka。推送协议为:https://github.com/Terry-Mao/goim/blob/master/doc/push.md

4、Router模块:会话信息管理模块

1)、处理Logic对Router的远程RPC调用,将用户的信息存入Bucket。

5、Job模块:消息转发模块

1)、接收从Kafka队列中传入的数据,根据数据的类型(单播、多播、广播、按房间推送)等消息,通过RPC调用Conet模块,将消息发送给客户端。

四、goim的设计实现

1、Comet主要功能实现(核心)

1)、概述

        a、Comet模块一方面维护客户端的连接、另一方面RPC调用Logic模块进行身份校验然后使用HTTP另一方面去接收Job的RPC调用,然后将消息再通过原有发送回去。

        b、支持WebSocket、TCP的方式与客户端交互,因此需要与协议解耦。

2)、流程(以下以TCP为主:项目主要使用TCP)

        a、初始化DefaultServer(整个Comet服务的核心成员),初始化监听TCP端口、初始化监听WebSocket端口、初始化PushRPC服务(供Job模块调用)、初始化RPC客户端(负责调用Logic的RPC服务)。

        b、启动服务器核心个数个Goroutine用于监听并连接客户端请求。

        c、一个连接过来以后,握手认证(需要自己实现),首先调用authTCP去RPC调用Logic模块,获取roomid、channel(TCP连接封装在Channel中)、subkey(作为会话标识),然后通过subkey,通过cityHash从Buckets(减少锁的争用)中获取到当前subkey的bucket,进而把数据(roomid、channel、subkey)存入bucket。客户端会经常发送心跳给服务器,comet以此判断是否在线。

        d、当Job模块RPC调用Comet,将消息发送过来,通过拆解消息找到Subkey,以此找到bucket、channel,进而将消息通过channel中封装的conn将消息转发给客户端。

3)、流程图

4)、重要结构体分析

        a、Bucket:  一个Comet服务默认拥有1024个Bucket, 这样可以减少锁争夺。可以将Bucket理解为进行会话管理的结构体, Bucket持有channel的map,以及Room的map。 

        b、Room: 即房间内所有的Channel,使用双链表存储所有的Channel。

        c、Channel: TCP连接封装在Channel里面,使用CliProto来进行拆包、拼包。

        d、Proto: 自定义协议的封装。

2、Logic主要功能实现

1)、概述

        Logic主要负责业务处理,可以在此模块进行功能扩展。

        a、logic 模块是comet 模块调用的,接受 comet 模块的命令,然后进行处理,再发送的消息的kafka队列上,

        b、同时RPC调用 router 模块,记录用户的 uid server room 等信息。同时获得router模块的信息。

2)、流程逻辑(主要的逻辑在http.go中,以推送一条消息为例)

        a、初始化RPC客户端负责调用Router模块、初始化供Comet调用的RPC服务端,开启HTTP监听服务,负责消息的接收。初始化Kafka,负责处理完的消息转发至Kafka。

        b、http接收到POST请求后,从URL中获取到要发送给的用户的Subkey。然后根据Subkey将消息通过Kafka转发出去。

3)、流程图

3、Router主要功能实现

1)、概述

userId和serverId(comet服务器地址)的关系,userId和roomId的关系,注册用户roomId为固定1,非注册用户roomId为-1。

        b、logic连接router需要一致性hash,所以不能随意添加router服务器。

2)、流程逻辑(主要的逻辑功能都在rpc.go中,以注册、验证用户为例)

        a、Logic模块RPC调用RouterRPC的Put方法,首先根据参数中的UserID从Buckets找到Bucket

        b、Bucket根据参数中的userID从Sessions找到Session获取到Subkey.

3)、重要结构体分析

        a、Bucket:Bucket是Session的容器,为了减少锁争夺,会有多个Bucket,根据用户id与Bucket数量进行mod运算来确定。

        b、Session:客户端会话信息管理,以用户id为单位,即每个用户有且拥有一个session,session包含了用户每个连接的comet service信息,以及每个连接所属的roomid。

4、Job功能主要实现

1、概述

        a、主要是消费Kafka中的数据,然后RPC调用Comet,进行数据的中转。

2、流程逻辑(主要的代码逻辑集中在push.go)

        a、初始化RPC客户端,远程调用Comet,启用一个Goroutine负责与Comet服务器进行消息同步,初始化Kafka从中读取数据。

        b、初始化Kafka以后,会一直从Kafka中读取数据,当一个消息过来以后,判断消息的类型,调用不同的RPC方法。

五、优缺点分析

1、数据结构

1)、Router模块中Bucket是Session的容器,为了减少锁争夺,使用了多个Bucket,根据用户id与Bucket数量进行mod运算来确定,这个Session放到哪个Bucket中。

优点:减少锁争夺,并发会提高。缺点:导致Router变的有状态,无法扩容。综合考虑:router只是放一些用户数据,没有过多操作。单机或固定多机,不需要考虑动态扩容,因为性能的瓶颈不会明显。

2)、Router模块中,Cleaner是与Session一一对应的一个结构,用于清理Session信息,每个Bucket会有一个单独的gorutine进行定时清理,使用了一条双向循环链表来记录当前所有Session的信息,为了克服移除一个节点时需要遍历链表,额外用了一个map来快速定位到节点,然后操作这个节点的指针来进行删除。

3)、Comet模块中,RingBuf是一个环形缓冲区,其中保存的是空闲的proto对象,负责TCP数据的拆包封包。可参考(http://ifeve.com/dissecting-disruptor-whats-so-special/

2、功能缺失

1)、goim只是提供一个im系统的框架实现,很多细节,需要自己实现,目前看到只有Auth这块提供了相应的接口,可以在不侵入原有代码的基础上进行开发。大部分的功能,还需要进行二次开发。

2)、没有提供离线消息的处理。可以使用Kafka的机制进行消息的存储,当客户端登陆以后再发送。或者改造logic模块。

3)、没有提供消息的持久化。可以从kafka抽取一份数据来转存,利用kafka pub/sub group的特性来实现

4)、用户间的发送消息,只能直接调用Logic模块。没有在Comet模块下进行消息的转发,Comet只是提供了认证。

5)、不能随意扩展Router节点,需要在开始的时候预测号服务器熟练。router节点都挂掉,没有好的方案解决带来的无法找到接收端用户信息的问题。不选择redis代替router作者解释是因为有同一userId多次连接序号分配问题以及原子操作。https://github.com/Terry-Mao/goim/issues/33

6)、goim不能保证消息的可靠投递。即六个报文完成消息的可靠投递。也就没有重传,去重等功能。http://www.52im.net/thread-294-1-1.html

7)、goim消息的有序性,只是通过Kafka所具有的队列特性保证消息的一部分的有序性,不能完全保证消息的有序性。

3、引用过多的技术栈

1)、使用Kafka作为消息队列。因此必须使用Zookeeper,必须在服务器上安装Java。Go生态下可以使用nsq。(作者好像不建议使用)

4、代码风格

1)、将方法中所有会用到的变量在方法开始处定义。可以解决每次是使用:= 还是=的问题。

5、功能实现

1)、使用RPC进行心跳检查。

2)、用户的注册与消息的处理放在Logic模块下,个人觉得这部分还是单独抽一个模块处理为好。

六、IM架构设计

七、总结

    IM系统模块拆分非常重要,goim使用四个模块可以保证一部分模块的动态扩展,模块的热重启,提高系统可用性。

    goim我个人认为其无法保证IM系统的完美运行。无法保证消息的必达、有序(只是消息队列方面的有序),离线处理等对于IM系统最为关键的指标。作为一个推送系统或许更加合适(但是推送系统不需要这么复杂的架构)。

    今后的开发过程中,首要解决的就是业务的拆分问题,把聊天这种需要及时操作的动作跟联系人、聊天记录这种非同步操作分开。

    对goim添加了很多注释,方便理解,如有需要访问https://github.com/Zereker/goim