基于Telegram二次开发 --- MTProtoKit 消息事务

image.png

上图是一个通用消息处理序列图,我们接下来就将这个原型展开,进行一个比较全面的分析。

消息服务实现

消息服务,它提供了处理和消息相关的一组方法,比如我们发送一个 RPC远程过程调用)请求。这里使用 MessageService 来作为抽象名称,我觉得是因为比较容易进行泛化,特定性约束不强,只要和消息相关的操作,都可以以此来展开实现。因为 MTProto 的定义是一个非常强大的类,它能给所有消息相关操作提供它们想要的任何支持,这样的定义好坏是显而易见的。在目前的 MTProtoKit 中,大致提供了以下几个消息服务:

1、MTTimeSyncMessageService - 时间同步

在 Telegram 的协议中,每个消息标识都附带了系统时间信息,收到的消息标识符中是服务端时间,而发送的消息标识符中是客户端时间。客户端生成消息标识符的算法,在 MTSessionInfo 中如下:

- (int64_t)generateClientMessageId:(bool *)monotonityViolated
{
    int64_t messageId = (int64_t)([_context globalTime] * 4294967296);
    
    if (messageId < _lastClientMessageId)
    {
        if (monotonityViolated != NULL)
            *monotonityViolated = true;
    }
    
    if (messageId == _lastClientMessageId)
        messageId = _lastClientMessageId + 1;
    
    while (messageId % 4 != 0)
        messageId++;
    
    _lastClientMessageId = messageId;
    return messageId;
}

当服务端意识到和客户端时间相差较大,则会忽略掉客户端发送来的消息,而这个服务便是用来和服务端时间进行校准。实现逻辑也比较简单,它会主动向服务端发送若干个消息进行时间采样,最终去除相差最小和相差最大的两个采样来求平均值。

这个时间同步服务,是直接由 MTProto 调用的(requestTimeResync),所以这里的逻辑依赖关系有点紊乱,我们使用类图梳理一下:

image.png

这里有个明显的互相依赖,MTTimeSyncMessageServiceMTProto 的观察者,并使用了它的相应方法;MTProto 亦是 MTTimeSyncMessageService 的观察者,也使用了它的相应方法;这里之所以这么设计,是因为同步服务必须依赖于 MTProto 提供的强有力后盾,但 MTProto 又必须要确保消息时间的准确性,于是乎就造成了这样的格局。

2、MTRequestMessageService - RPC 请求和响应

这是一个使用非常频繁的服务,主要是用来向服务端发送 RPC 请求,并负责处理超时、错误、回执等。

2.1 消息打包

这是一个比较有用的特性,每一个 MTRequest 都会携带一个它需要发送的消息数据,然后添加到 RPC 服务(MTRequestMessageService)中,此时 RPC 服务会请求 MTProto 进行事务传输,但 MTProto 需要进行一些另外的准备和检验操作,所以可能会晚点才能向 RPC 服务要求构建事务,这时候 RPC 服务中可能会积累多个 MTRequest,于是在构建事务的时候,事务的 payload 里就会有多个消息。同理,MTProto 在请求真正的向外传输时,又有可能会积累多个需要传输的事务,因为底层传输支持也需要做一些其他额外的处理。

针对上诉情况,Telegram 的 MTProto 中有一个消息容器的概念,它可以将多个消息放置到一个容器里,一同发送到服务器,服务器亦会对消息容器里需要响应的消息进行打包响应。这样就减少了网络传输的次数,也提高了响应的及时性(减少了排队请求的可能性)。

2.2 依赖处理

针对上诉的打包特性,它隐性的引入了一个问题,也就是时序问题,有些消息是必须在某些消息前得到处理的。所以,Telegram 增加了消息依赖的特性,它可以指定某个消息必须在另外一个消息前得到执行,这会对并发处理的服务端有很好的提示,但必然的增加了客户端实现的复杂度。

2.3 超时管理

由于消息可能会被打包处理,所以在超时管理上亦会跟一般超时处理不同,首先会在真的进入发送阶段前进行检测,其次是在收到响应时再做检测。值得一提的是,这里超时时钟使用的是 MTAbsoluteSystemTime,它是一个取 CPU 频率计算的高精度时钟,以下是 精准时钟的实现

#import <MtProtoKit/MTTime.h>

#import <mach/mach_time.h>

CFAbsoluteTime MTAbsoluteSystemTime()
{
    static mach_timebase_info_data_t s_timebase_info;
    if (s_timebase_info.denom == 0)
        mach_timebase_info(&s_timebase_info);
    
    return ((CFAbsoluteTime)(mach_absolute_time() * s_timebase_info.numer)) / (s_timebase_info.denom * NSEC_PER_SEC);
}
2.4 错误处理

除了响应的 RPCError 之外,MTProto 在对消息进行标识符编码的时候,还会检查标识符的唯一性,因为标识符和系统时间息息相关,所以如果小于上个消息标识符,则说明唯一性被破坏了,亦说明了系统时间有问题。发生这样的情况,MTProto 会重置当前的 Session,并进行时间同步,也就是使用了 MTTimeSyncMessageService。而这样的消息,会在本次传输中被抛弃掉,切换完 Session 后,才会继续发送。

3、MTResendMessageService - 消息重传

这算是 MTProto 比较有特性的另一个服务,这里的消息重传,并不是指客户端发送消息出现错误而进行后续的重新请求,而是指当客户端向服务器发出 RPC 请求后,服务端检测到这是一个重复的请求(消息标识符相同),如果响应内容较小,服务端会直接返回结果,而如果响应内容较大,此时服务端会回馈一个 MTMsgDetailedResponseInfoMessage,如果想要取得相应结果,则需要使用该服务,将请求消息标识符重新发送到服务器。

这个服务和时间同步服务一样,是由 MTProto 直接使用的,涉及到的核心代码如下:

- (void)_processIncomingMessage:(MTIncomingMessage *)incomingMessage totalSize:(int)totalSize withTransactionId:(id)transactionId address:(MTDatacenterAddress *)address authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector {
// ... 略
       if (shouldRequest)
        {
            [self requestMessageWithId:detailedInfoMessage.responseMessageId];
            if (MTLogEnabled()) {
                MTLogWithPrefix(_getLogPrefix, @"[MTProto#%p@%p will request message %" PRId64 "", self, _context, detailedInfoMessage.responseMessageId);
            }
            MTShortLog(@"[MTProto#%p@%p will request message %" PRId64 "", self, _context, detailedInfoMessage.responseMessageId);
        }
        else
        {
            [_sessionInfo scheduleMessageConfirmation:detailedInfoMessage.responseMessageId size:(NSInteger)detailedInfoMessage.responseLength];
            [self requestTransportTransaction];
        }
// ... 略       
}

实例方法 requestMessageWithId

- (void)requestMessageWithId:(int64_t)messageId
{
    bool alreadyRequestingThisMessage = false;
    
    for (id<MTMessageService> messageService in _messageServices)
    {
        if ([messageService isKindOfClass:[MTResendMessageService class]])
        {
            if (((MTResendMessageService *)messageService).messageId == messageId)
            {
                alreadyRequestingThisMessage = true;
                
                break;
            }
        }
    }
    
    if (!alreadyRequestingThisMessage && ![_sessionInfo messageProcessed:messageId])
    {
        MTResendMessageService *resendService = [[MTResendMessageService alloc] initWithMessageId:messageId];
        resendService.delegate = self;
        [self addMessageService:resendService];
    }
}

4、MTDatacenterAuthMessageService - 数据中心授权

这也是一个非常重要的服务,它和用户授权息息相关,首先我们要清楚什么是 DataCenter,也就是数据中心;可以简单的把一个数据中心就当成一台完整的服务器,我们可以对它进行发送任何合理的请求;Telegram 的数据中心遍布在全球各地,而它们之间的数据同步是对客户端透明的,客户端要做的就是选择一个最适合自身的数据中心;数据中心地址的查找,在 MTProtoKit 中被封装在了 MTDiscoverDatacenterAddressAction 中,而后由全局上下文 MTContext 进行调用。

那么这个授权服务,它所做的便是向特定的数据中心发出授权请求,完成一个授权的全过程;整个授权的加密过程都在这个服务中体现出来,它采用的是基于 nonce 的一个认证体质,在安全领域中,nonce 是指在一个特定的上下文中,仅仅只被使用一次的数。通过使用 nonce,我们可以防御 Replay attack回放攻击)和 Chosen-Plaintext attack选择明文攻击);Telegram 同时使用了客户端 nonce 和服务端 nonce,并且加入了 DH 值校验,所以安全程度是非常高的。大体流程如下图:

image.png

在这个服务类的具体实现里,很容易可以看来,它是一个状态机,随着授权环节的推进,当前状态进行相应的推进。而使用这个服务的,是另一个封装类 MTDatacenterAuthAction,和上面说过的那个数据中心查找类类似,它们都采用了 Command 设计模式,也都是由全局上下文进行管理、调用。

5、MTTransport - 数据传输

这是所有数据传输的基础服务,它的主要职责即是传输和接受数据,并且还监听网络可用性变化。这算得上是一个抽象类,它只保留了 MTTcpTransport 一个子类实现,很显然,是基于特定协议的实现。

MTTransport 的设计也稍显复杂,虽然它是由 MTProto 直接使用的,但却是由全局上下文进行统一管理。在 MTTransport 之上还有另一个更高层级的抽象 MTTransportScheme,这个类是用来描述一种特定的传输格式,并且可以根据这个特定的格式构建出合适的 MTTransport

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容