上回我们对TeamTalk做了一个比较详细的架构分析,并得到了蘑菇街的官方回复,这种态度还是值得很多公司技术运营学习的。本打算对Telegram的Mac客户端也做一次架构分析的,但发现它的源码比较杂乱,里面混合很多有用没用的文件,另外它的抽象方式也是非常奇怪,继承体系也不是很合理,大致看了下,我就决定放弃了。而无意间却让我点开了它里面所依赖的一个Sub Module:MTProtoKit,我发现这个项目和Mac客户端项目完全是两个水准,所以,我决定单独把它拿出来做次分析。
首先,你应该要知道什么是Telegram,它和TeamTalk一样,都是开源的IM,百度搜索一下会有一些关于它的介绍。以下是和本篇博客有关的地址链接:
我说过,要想提高自身的设计能力,阅读开源项目是很好的途径。在分析别人源码的过程中,我们可以汲取到一些其它的思想,架构设计这条路是没有止境的,我们唯有不断学习,才能成为更好的自己!那么,我们开始本次的分析之旅吧!
MTProto 简介
既然我们要对这个MTProtoKit进行分析,我们首先应该要清楚,这个MTProto
到底是个什么东西?在官网上不难看到,官方给出了以下定义:
The protocol is designed for access to a server API from applications running on mobile devices. It must be emphasized that a web browser is not such an application.
也就是说,MTProto
是一个Telegram自定义的通讯协议,用于移动端App与服务器交互数据使用。这里还特别指出了,一个浏览器不能算是移动端App,所以这个协议不适用于HTML5之类的网页客户端。
现在我们大体知道了,MTProto
是一种协议,那么MTProtoKit就是对该协议实现的封装,使得我们能够更方便的使用这样的协议。了解了这些,对我们接下来的分析会有很好的帮助。
宏观分析
拿到这样一个项目,如果一开始就拘泥于细节实现,那么很难快速的了解它的一些核心信息。任何软件都是从无到有,从细小变得庞大,所有复杂的架构都会有一个简单的核心原型,而后进行增量迭代。所以,我们只要找到它的核心原型,再站在设计者得角度进行考虑、分析,任何复杂的软件体系,都可以顺藤摸瓜的理清一条思路。
MTProtoKit复杂么?其实它并不复杂,但我觉得依然需要使用从整体到细节这样的方式来分析。首先,我们看看它的一个核心原型类图:
这个核心原型还是相当简单的,主要有以下几个抽象:
- MTProto:这可能是最核心的一个类了,所以设计者给了它这样一个看起来就非常重要的名字(_虽然我并不推荐这样命名_)。它主要负责管理消息服务(_MTMessageService_),给予消息服务适当的运行上下文,并且负责传输由消息服务所构建的消息事务(_MTMessageTransaction_)。实话说,这个类设计得有点臃肿,以至于一眼看上去,你都不知道它是干什么的。
- MTMessageService:消息服务,这是另外一个非常重要的抽象,它是一个协议,也就是大多数高级语言里面的接口。这个协议是
MTProto
的一个Observer
,也就是说,它会监听MTProto
所有发布的事件,以此来控制自身实现逻辑。 - MTMessageTransaction:消息事务,这是非常高级的一个特性,它内部保持了一系列需要发送的消息,并提供了一个所有消息发送完毕的回调。
最核心的应该也就这三个类了,MTMessageService
是暴露给最终使用者的,使用者用它发送消息,而它在内部将要发送的消息按照服务的特定实现构建成MTMessageTransaction
,然后请求MTProto
将这个MTMessageTransaction
进行传输,并且监听MTProto
收到消息的回调,来进一步完成一整个消息处理流程。下面是一个通用消息处理序列图:
通过这样一个大体的分析,我们应该能够清楚怎样继续来寻根究底了。那么,我们接下来就将这个原型展开,进行一个比较全面的分析。
消息服务实现
消息服务,它提供了处理和消息相关的一组方法,比如我们发送一个RPC(_远程过程调用_)请求。这里使用MessageService
来作为抽象名称,我觉得是因为比较容易进行泛化,特定性约束不强,只要和消息相关的操作,都可以以此来展开实现。因为MTProto
的定义是一个非常强大的类,它能给所有消息相关操作提供它们想要的任何支持,这样的定义好坏是显而易见的。在目前的MTProtoKit中,大致提供了以下几个消息服务:
MTTimeSyncMessageService - 时间同步
在telegram的协议中,每个消息标识都附带了系统时间信息,收到的消息标识符中是服务端时间,而发送的消息标识符中是客户端时间。客户端生成消息标识符的算法,在MTSessionInfo
中如下:
1 | - (int64_t)generateClientMessageId:(bool *)monotonityViolated |
当服务端意识到和客户端时间相差较大,则会忽略掉客户端发送来的消息,而这个服务便是用来和服务端时间进行校准。实现逻辑也比较简单,它会主动向服务端发送若干个消息进行时间采样,最终去除相差最小和相差最大的两个采样来求平均值。
这个时间同步服务,是直接由MTProto
调用的(_requestTimeResync_),所以这里的逻辑依赖关系有点紊乱,我们使用类图梳理一下:
这里有个明显的互相依赖,MTTimeSyncMessageService
是MTProto
的观察者,并使用了它的相应方法;MTProto
亦是MTTimeSyncMessageService
的观察者,也使用了它的相应方法。从类设计的角度来说,这样的耦合状态是我们不愿意看到的,因为它提高了以后的维护成本。这里之所以这么设计,是因为同步服务必须依赖于MTProto
提供的强有力后盾,但MTProto
又必须要确保消息时间的准确性,于是乎就造成了这样的格局,但,肯定是可以通过改变结构来解除这样的紊乱。
MTRequestMessageService - RPC 请求和响应
这是一个使用非常频繁的服务,主要是用来向服务端发送RPC请求,并负责处理超时、错误、回执等。从它这么多的职责中,就可以看出它会是一个比较巨大的类,一般人是难以Hold住的,但将职责拆分细化又会增加结构上的复杂度,所以这是我们在进行设计的时候需要权衡的。
这里完整消息处理的复杂度还是蛮高的,因为有很多处理细节,要想完整阐述清楚也不容易。所以,还是很佩服telegram这样严谨的协议设计,使得MTProto
能适应很多不同场合,相比之下,我所使用或设计过的协议,大多都逊色很多。这里,我只列出一些比较关键的处理点,更多细节,有兴趣的自己去深挖吧!
消息打包
这是一个比较有用的特性,每一个
MTRequest
都会携带一个它需要发送的消息数据,然后添加到RPC服务(_MTRequestMessageService_)中,此时RPC服务会请求MTProto
进行事务传输,但MTProto
需要进行一些另外的准备和检验操作,所以可能会晚点才能向RPC服务要求构建事务,这时候RPC服务中可能会积累多个MTRequest
,于是在构建事务的时候,事务的payload里就会有多个消息。同理,MTProto
在请求真正的向外传输时,又有可能会积累多个需要传输的事务,因为底层传输支持也需要做一些其他额外的处理。针对上诉情况,telegram的MTProto中有一个消息容器的概念,它可以将多个消息放置到一个容器里,一同发送到服务器,服务器亦会对消息容器里需要响应的消息进行打包响应。这样就减少了网络传输的次数,也提高了响应的及时性(_减少了排队请求的可能性_)。
依赖处理
针对上诉的打包特性,它隐性的引入了一个问题,也就是时序问题,有些消息是必须在某些消息前得到处理的。所以,telegram增加了消息依赖的特性,它可以指定某个消息必须在另外一个消息前得到执行,这会对并发处理的服务端有很好的提示,但必然的增加了客户端实现的复杂度。
超时管理
由于消息可能会被打包处理,所以在超时管理上亦会跟一般超时处理不同,首先会在真的进入发送阶段前进行检测,其次是在收到响应时再做检测。值得一提的是,这里超时时钟使用的是
MTAbsoluteTime
,它是一个取CPU频率计算的高精度时钟。错误处理
除了响应的RPCError之外,
MTProto
在对消息进行标识符编码的时候,还会检查标识符的唯一性,因为标识符和系统时间息息相关,所以如果小于上个消息标识符,则说明唯一性被破坏了,亦说明了系统时间有问题。发生这样的情况,MTProto
会重置当前的Session
,并进行时间同步,也就是使用了MTTimeSyncMessageService
。而这样的消息,会在本次传输中被抛弃掉,切换完Session
后,才会继续发送。
MTResendMessageService - 消息重传
这算是MTProto比较有特性的另一个服务,所以,telegram对这个协议的确是花费了不少心思,也不枉费我把这个周末的时间花在了这上面。这里的消息重传,并不是指客户端发送消息出现错误而进行后续的重新请求,而是指当客户端向服务器发出RPC请求后,服务端检测到这是一个重复的请求(_消息标识符相同_),如果响应内容较小,服务端会直接返回结果,而如果响应内容较大,此时服务端会回馈一个MTMsgDetailedResponseInfoMessage
,如果想要取得相应结果,则需要使用该服务,将请求消息标识符重新发送到服务器。
这个服务和时间同步服务一样,是由MTProto
直接使用的,涉及到的核心代码如下:
1 | - (void)_processIncomingMessage:(MTIncomingMessage *)incomingMessage withTransactionId:(id)transactionId { |
MTDatacenterAuthMessageService - 数据中心授权
这也是一个非常重要的服务,它和用户授权息息相关,首先我们要清楚什么是DataCenter,也就是数据中心。可以简单的把一个数据中心就当成一台完整的服务器,我们可以对它进行发送任何合理的请求。telegram的数据中心遍布在全球各地,而它们之间的数据同步是对客户端透明的,客户端要做的就是选择一个最适合自身的数据中心。数据中心地址的查找,在MTProtoKit中被封装在了MTDiscoverDatacenterAddressAction
中,而后由全局上下文MTContext
进行调用。
那么这个授权服务,它所做的便是向特定的数据中心发出授权请求,完成一个授权的全过程。整个授权的加密过程都在这个服务中体现出来,telegram号称它是非常安全的IM,从这里的源码中可以看出,它的确没有撒谎。它采用的是基于nonce的一个认证体质,在安全领域中,nonce是指在一个特定的上下文中,仅仅只被使用一次的数。通过使用nonce,我们可以防御Replay attack(_回放攻击_)和Chosen-Plaintext attack(_选择明文攻击_)。关于安全相关的知识,我这里就不展开了,有兴趣的同学可以自己去查阅相关资料。telegram同时使用了客户端nonce和服务端nonce,并且加入了DH值校验,所以安全程度是非常高的。大体流程如下图:
在这个服务类的具体实现里,很容易可以看来,它是一个状态机,随着授权环节的推进,当前状态进行相应的推进。而使用这个服务的,是另一个封装类MTDatacenterAuthAction
,和上面说过的那个数据中心查找类类似,它们都采用了Command
模式进行设计,也都是由全局上下文进行管理、调用。
MTTransport - 数据传输
这是最后一个实现了MTService
的类,也是所有数据传输的基础服务,它的主要职责即是传输和接受数据,并且还监听网络可用性变化。这算得上是一个抽象类,它有两个子类实现MTTcpTransport
和MTHttpTransport
,很显然,是基于特定协议的实现。
MTTransport
的设计也稍显复杂,虽然它是由MTProto
直接使用的,但却是由全局上下文进行统一管理。在MTTransport
之上还有另一个更高层级的抽象MTTransportScheme
,这个类是用来描述一种特定的传输格式,并且可以根据这个特定的格式构建出合适的MTTransport
。而确定这种传输格式,是通过另一Command类MTDiscoverTransportSchemeAction
,它可以发现某个数据中心支持的传输格式,并可以挑选出最优的结果。
这里的实现细节,就不一一展开了,大体清楚了他们的意图,我觉得也就够了。
内置消息简析
在MTProtoKit中,有很多内置的消息,定义在Serialization目录下,这些基本都是和协议相关的PONSO。除了这些消息之外,还有和解析这些消息相关的类,比如MTBufferReader
和MTInternalMessageParser
,这些都是用来对这些内置消息进行解析用的。除了内置消息,还应该有很多业务相关性的消息,而这些消息都不在MTProtoKit考虑之中,MTProtoKit将其它消息的序列化由MTSerialization
协议委托给了使用者去实现,这样做还是很合理的,因为MTProto是一个非常动态的协议,扩展性非常强。
内置消息其实就像编程语言给我提供的标准库一样,它是框架,也是基础。下面我简单选取一些消息做个介绍:
- **MTBadMsgNotificationMessage**:服务端未能正确解析客户端消息时,会返回该消息,并附带错误码。
- **MTBadServerSaltNotificationMessage**:服务端对Salt验证失败时,会返回该消息。
- **MTDestroySessionResponseOkMessage**:客户端请求销毁当前会话,服务端返回销毁成功的响应。
- **MTDestroySessionResponseNoneMessage**:客户端请求销毁当前会话,服务端返回未找到该会话的响应。
- **MTDropRpcResultUnknownMessage**:客户端请求服务端取消某次RPC请求,服务端返回未知状态的响应。
- **MTDropRpcResultDroppedRunningMessage**:客户端请求服务端取消某次RPC请求,服务端返回正在处理的响应。
- **MTDropRpcResultDroppedMessage**:客户端请求服务端取消某次RPC请求,服务端返回处理完成的响应。
- **MTFutureSaltsMessage**:客户端请求服务端Salt,服务端的响应消息。
- **MTMsgsStateReqMessage**:当消息处理的任何一方(_服务器或客户端_)长时间未收到发出消息的响应,则可以通过该请求来查询消息处理状态。
- **MTMsgsStateInfoMessage**:消息处理状态响应。
- **MTMsgAllInfoMessage**:自发性的通知另一方,消息处理的状态。
- **MTMsgContainerMessage**:消息容器,用于多个消息同时打包发送。
- **MTMsgDetailedResponseInfoMessage**:当收到重复的消息请求时,且响应内容过大,服务端会返回该条响应,用于描述响应内容。
- **MTMsgResendReqMessage**:客户端请求消息响应重传,旨在处理重复请求时,明确的让服务端返回响应内容。
- **MTMsgsAckMessage**:消息回执。
- **MTNewSessionCreatedMessage**:服务端通知客户端,一个新的会话被创建。
- **MTRpcResultMessage**:RPC请求响应消息。
- **MTRpcError**:RPC请求错误响应。
全局上下文
所谓的全局上下文,也就是MTContext
类,这是一个使用相当频繁的类,它的主要意图是用来给MTProtoKit中,其它类提供一个公共的运行上下文,也相当于是整个MTProtoKit的入口点。所以,一些公共的状态和方法都会被提升到这个类中,它大体记录了以下信息:
- 客户端运行环境,即
MTApiEnvironment
。 - 非内置消息的序列化器实现,即
MTSerialization
协议的实现。 - 客户端时间,并允许设定偏差来校准。
- 当前用户的授权相关信息和操作。
- 数据中心的相关信息和操作。
- 传输格式(_MTTransportScheme_)的相关信息和操作。
这个全局上下文的实现,倒是没有太多可以展开的,有兴趣深入研究的,可以自行去观看。
其它细节
现在,我们再来看看一些比较有意思的细节处理。首先是MTBuffer
类中,字节对齐的算法实现:
1 | static inline int roundUp(int numToRound, int multiple) |
很简单的算法,但我觉得很有意思,使用roundUp(17, 4)
,则会得到17按照4向上对齐的结果,也就是20。那么,怎么实现roundDown
呢?这么简单的问题,大家自己动手去实现吧!
MTBuffer
中,还有一个方法,也就是追加TL字节,我们来看一下:
1 | - (void)appendTLBytes:(NSData *)bytes { |
当字节长度大于或等于254时,这样的处理是不是有点让人困惑呢?其实是这样的,当这个块的长度小于254时,第一个字节就是用来标识内容的长度;而当这个块的长度大于或等于254时,第一个字节只是一个标志,后面3个字节才是真正的长度(_考虑一下大小端的问题哦_),所以,每个块的最大长度是24位值,而不是32位。这样做,长度值所占用的字节就可以被压缩了,大家再考虑下,为什么要按4字节进行对齐呢?优化内存布局?
再来看一个MTInternalMessageParser
中的decompressGZip
方法,因为消息是可以放置在gzip容器中进行传输的,所以客户端需要解压字节流。
1 | + (NSData *)decompressGZip:(NSData *)data { |
再看精确时钟的实现:
1 | MTAbsoluteTime MTAbsoluteSystemTime() |
好了,这些都是可以在我们设计网络通讯协议的时候拿来借鉴的,他山之石,可以攻玉啊!学习,才能成为更好的自己啊!
总结
本次分析,只能算是比较粗略的将核心内容过了一遍,由于时间和精力问题,并没有分析的非常透彻,但,收获依然很多。telegram在协议设计上面,还是做到了非常专业,细节处理得也都非常完善,这些都是值得我们去深思和探讨的,毕竟网络通讯中异常杂多,而细节终会决定成败。
MTProto协议里,还有一块非常重要的部分,也就是用来描述该协议的TL Language
,这也是非常有意思的设计。如果不出意外,下一篇博文中,我会来简略的谈谈这个TL Language
,借此,我们可以看一看,这样一个可扩展的协议它的元数据是如何描述的,并更能体会到这个Protocol的强大!
那么,拭目以待吧!