Telegram-iOS 源码分析:第四部分(MTProto)

版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。

如需查阅原作者文章,附赠原文章机票

正如在上一篇文章中所阐述的,TCP是Telegram-iOS上仅存的MTProto传输。让我们继续分析MTProto连接管理的实现细节。

网络部分的代码主要位于模块TelegramCoreMTProtoKit中。在正文开始前,我们先来看一个简单的问题:

在首次登录过程中MTProtoKit使用了多少个连接?

结果让我感到惊讶:20个TCP连接到Telegram的数据中心以及8个HTTPS拓展服务请求。常见的最佳做法是使用尽可能少的连接。

在深入研究代码以揭示谜题之前,先介绍一些重要的概念。

1.连接的基本概念

数据中心

Telegram 将其后台服务器分为5个数据中心。每个数据中心都有自己的ID和别名。别名用于撰写用于HTTP传输的URI,iOS应用程序中未使用到。Telegram后端将每一个注册帐户都关联到一个主要到数据中心。它要求客户端使用最合适的主数据中心来访问用户数据,并且可能使用其他数据中心来下载图片,文件等。

DC 1, pluto
DC 2, venus
DC 3, aurora
DC 4, vesta
DC 5, flora

每个数据中心可以通过多个IP地址连接。不直接使用域名通常有以下几个原因:

  • 系统DNS服务可能不稳定,甚至不可信。
  • IP地址和端口需要经常更改以应对网络问题。静态IP在某些地区可能无法访问,可以部署弹性IP来代理数据中心的流量。应用程序能够及时更新其端点配置。
  • Geo DNS之类的解决方案非常适合粗粒度的IP选择,后台直接控制比较好。

Telegram-iOS内置了几个用于冷启动的种子地址:

let seedAddressList: [Int: [String]]
seedAddressList = [
    1: ["149.154.175.50", "2001:b28:f23d:f001::a"],   //AS59930
    2: ["149.154.167.50", "2001:67c:4e8:f002::a"],    //AS62041
    3: ["149.154.175.100", "2001:b28:f23d:f003::a"],  //AS59930
    4: ["149.154.167.91", "2001:67c:4e8:f004::a"],    //AS62041
    5: ["149.154.171.5", "2001:b28:f23f:f005::a"]     //AS62014
]

Telegram拥有四个用于发布IP的AS号:AS62014,AS62041,AS59930和AS44907(如果您感兴趣,可以通过搜索AS号找到更多静态IP)。

端点发现

Telegram-iOS可以通过内部和外部服务更新端点。这些方法作为其他方法的补充,以最大限度地提高更新的成功率。结果通过keydatacenterAddressSetById保存在Keychain中。

  • 通过Google Public DNSJSON APIHTTPS上进行DNS-over-HTTPS。tapv3.stel.com是要解析的主机名,并且已设置random_padding,该参数以防止可能的旁通道攻击。请求和响应的示例如下:
// https://dns.google.com/resolve?name=apv3.stel.com&type=16&random_padding=Fw8ZQonqP0qOqoa
{
  "Status": 0,
  "Question": [
    {
      "name": "apv3.stel.com.",
      "type": 16
    }
  ],
  "Answer": [
    {
      "name": "apv3.stel.com.",
      "type": 16,
      "data": "\"vEB1g6iW/a5RtZI/Rx33SEzLmRhz+vNenoY7iqAHW35plgToLfkNRVfvlaBsztOTeYSRqFko73rr2lumKmGax2biMcSQ==\""
    },
    {
      "name": "apv3.stel.com.",
      "type": 16,
      "data": "\"pEI+NHncHJCj9S0XzxhhTd3bkPteVxE5UQ8T06KCz0nP591un4Un82id0FyCEDF0BVmxMp+t673l3HAGD+fzR/qaJ1XpQ6KWxNpRLqA74m2UFTI1REP7ZczU2hmbURzSQvWQTxfp9tnGc1EnyqpUYphFb/Vi+sV83iaw6dTGOcKW1Kp/PW2xV99mmSFLBsspQRdUbKWvbrSpmXHbPbkSRZV61NvtaEiODG1We29nG58DUBqdW7m68ae11w\""
    }
  ]
}

Google service以多个DNS TXT内容作为响应,这些内容可以合并并转换为有效的base64字符串。客户端有RSA公钥解码数据并将其反序列化为MTBackupDatacenterAddress的列表。

代码内部有一个小窍门。除了正常的请求"dns.google.com"外,还会将Host header设置为另一个发送"https://www.google.com/resolve""dns.google.com"的请求,看起来好像是在将域前置到Google的一个子域,这使得DNS请求被伪装成像是正在访问谷歌搜索。Google于2018年4月宣布禁用域前置

  • Cloudflare。通过HTTPS进行DNS传输,实现类似于Google的解决方案。

  • CloudKit数据。对于登录用户,可以根据电话号码从CloudKit中获取相同的加密数据。

  • MTProto中的help.getConfig方法。如果客户端能够连接到任何数据中心,则此RPC请求可以获取包含DcOption列表的配置。

  • iOS PushKit和UserNotifications。远程通知中的payload可以包括数据中心的一个端点数据。

MTProto代理

除了由Telegram的工程团队操作的普通端点之外,Telegram还构建了一个代理系统,该系统允许第三方服务器代理其流量。作为交换,代理提供者需要将推广频道提供给其他用户。官方的代理代码是开源的。
作为不引入其他协议更改的反向代理,从客户端的角度来看,它与官方端点基本没有区别。

加密连接

除数据中心地址的IP和端口外,还提供一个可选参数secret,以指示客户端如何加密TCP连接。请注意,它与MTProto消息加密是两个不同的概念。它旨在混淆网络流量,这有助于应对DPI(深度数据包检测)。

加密有四种可能的类型:

  • nil。没有应用特殊的混淆。
  • MTProxySecretType0。像随机数据一样传输数据包是16字节的秘密。尽管数据包结构是隐藏的,但仍有一些DPI可以检测到的统计模式。
  • MTProxySecretType1。它是从17个字节的数据中解码出来的。第一个字节始终为0xdd,其他16个字节为机密。padded intermediate format被用来隐藏数据包模式。
  • MTProxySecretType2。启用了fake-TLS,从而使Telegram连接看起来像TLS v1.2连接。数据以字节0xee开头。接着16个字节是机密数据。其余的字节是一个UTF-8编码的字符串,它是在TLS握手期间使用的SNI域。

让我们以MTProto代理的共享URL为例。字符串以开头ee表明其加密类型是MTProxySecretType2。机密数据用零填充,伪造的域为itunes.apple.com

https://t.me/proxy?server=0.0.0.0
    &port=8080
    &secret=ee000000000000000000000000000000006974756e65732e6170706c652e636f6d
# 6974...6f6d can be decoded to "itunes.apple.com"

连接选择

由于一个数据中心可以具有一组地址,因此MTContext实施选择策略以选择具有最早故障时间戳的地址。

数据中心授权

在将MTProto消息发送到数据中心之前,需要完成目标DC的(p,q)授权。 AuthInfo在代码中称为数据中心身份验证信息。

用户授权

通过SMS代码或其他方法成功验证后,主数据中心将用户帐户与客户端的帐户相关联auth_key_id,从而授权以用户身份访问数据中心。如果客户端要使用相同的用户帐户访问其他数据中心,则需要提前转移授权。

回顾一下

根据概念,可知以下内容是客户端与后端交互的要求:

  • 客户端需要知道数据中心ID及其地址。
  • 地址可以通过端点发现来更新。
  • Telegram支持MTProto的特殊反向代理。
  • 如果地址可以访问,客户端需要在完成其他数据传输之前完成数据中心授权。
  • 客户端应通过其主数据中心完成用户授权。
  • 如果客户端需要使用用户帐户访问其他数据中心,则需要授权转移。

2.代码结构

让我们看一下Telegram-iOS如何构建代码的。如下图所示:


part-4-network_modules.png
  • 有一个依赖关系链,使UI控制器可以访问网络模块。大多数控制器依赖于一个帐户的数据模型,它要么实例AccountUnauthorizedAccount
  • 帐户类公开其Network实例字段,以供控制器发送请求。
public class UnauthorizedAccount {
    ...
    public let network: Network
    ...
}

public class Account {
    ...
    public let network: Network
    ...
}
  • Network封装与MTProtoKit模块的所有交互,并将RPC请求-响应对建模为Signals
/* Code snippets from Network.swift */
public final class Network: NSObject, MTRequestMessageServiceDelegate {
    ...
    func background() -> Signal
    
    public func request(
        _ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), 
        tag: NetworkRequestDependencyTag? = nil, 
        automaticFloodWait: Bool = true
        ) -> Signal
    ...
}

// a Signal operator to retry a RPC
public func retryRequest(signal: Signal) -> Signal

/* A code snippet to request login code from `Authorization.swift` */
// construct an MTProto API object
let sendCode = 
    Api.functions.auth.sendCode(
        flags: 0, 
        phoneNumber: phoneNumber, 
        currentNumber: nil, 
        apiId: apiId, 
        apiHash: apiHash)
// send the API via `network`
account.network.request(sendCode, automaticFloodWait: false)
  • MTProtoKit模块实现了端点信息,授权数据,连接生命周期,连接和协议加密等所有复杂逻辑

核心MTProtoKit类

  • MTContext。通过共享和维护所有数据中心的重要数据(例如地址,身份验证信息等),这是大多数MTProtoKit类的上下文。与当前设计相比,这不是一个单例,并且可能有多个实例。
  • MTProto。它是发送到特定数据中心的消息的核心管理器。
  • MTTcpTransport。它管理TCP连接。一个MTProto实例最多可以有一个传输活动。
  • MTMessageService。这是一个Objective-C协议,它定义了处理RPC的方法。一个MTProto实例可以有多个MTMessageService实例。

3.在首次登录过程中

典型的首次登录过程分为四个阶段:欢迎界面,电话号码界面,验证码界面和主界面。让我们看下每个阶段触发的连接数。

part-4-login-flow.png

提供了几张图来说明每个阶段的简化工作流程。有关图的一些注意事项:

  • 为了简化,省略了许多中间信号和类。
  • 橙色节点是Swift中的代码。
  • 绿色节点是Objective-C中的代码。
  • 蓝色边缘表示在此阶段创建的连接,而虚线表示请求数据中心身份验证信息的连接,而粗体表示表示RPC的连接。
  • 红色边缘表示H​​TTP请求。
  • 粉色的数据中心节点是通过端点发现更新的新IP地址,黑色的是种子地址。

首次启动

part-4-onboarding.png

首次启动该应用程序时,每个数据中心都没有帐户数据,也没有身份验证信息。一个新的UnAuthorizedAccountAsk实例MTContext从DC 1、2和4获取身份验证信息,这将创建TCP连接①②③。身份验证操作完成后,连接将关闭。

// UnauthorizedAccount.swift
public class UnauthorizedAccount {
    ...
    init(networkArguments: NetworkInitializationArguments, id: AccountRecordId, rootPath: String, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, shouldKeepAutoConnection: Bool = true) {
        ...
        network.context.performBatchUpdates({
            var datacenterIds: [Int] = [1, 2]
            if !testingEnvironment {
                datacenterIds.append(contentsOf: [4])
            }
            for id in datacenterIds {
                if network.context.authInfoForDatacenter(withId: id) == nil {
                    network.context.authInfoForDatacenter(withIdRequired: id, isCdn: false)
                }
            }
            network.context.beginExplicitBackupAddressDiscovery()
        })
    }
}

// Network.swift
context.setDiscoverBackupAddressListSignal(
    MTBackupAddressSignals.fetchBackupIps(
        testingEnvironment, 
        currentContext: context, 
        additionalSource: wrappedAdditionalSource, 
        phoneNumber: phoneNumber))

它还会调用beginExplicitBackupAddressDiscovery并启动在Network中设置的信号。fetchBackupIps结合了不同的发现信号,并且只需要第一个响应(多个请求,只要有一个请求有响应就行)fetchConfigFromAddress。如概念部分所述,它启动4个HTTP请求:①②向Google,③到Cloudflare和④到CloudKit。

+ (MTSignal * _Nonnull)fetchBackupIps:(bool)isTestingEnvironment currentContext:(MTContext * _Nonnull)currentContext additionalSource:(MTSignal * _Nullable)additionalSource phoneNumber:(NSString * _Nullable)phoneNumber {
    ...
    NSMutableArray *signals = [[NSMutableArray alloc] init];
    [signals addObject:[self fetchBackupIpsResolveGoogle:isTestingEnvironment phoneNumber:phoneNumber currentContext:currentContext addressOverride:currentContext.apiEnvironment.accessHostOverride]];
    [signals addObject:[self fetchBackupIpsResolveCloudflare:isTestingEnvironment phoneNumber:phoneNumber currentContext:currentContext addressOverride:currentContext.apiEnvironment.accessHostOverride]];
    [signals addObject:[additionalSource mapToSignal:^MTSignal *(MTBackupDatacenterData *datacenterData) {
            ...
        }]];
    return [[[MTSignal mergeSignals:signals] take:1] mapToSignal:^MTSignal *(MTBackupDatacenterData *data) {
        NSMutableArray *signals = [[NSMutableArray alloc] init];
        NSTimeInterval delay = 0.0;
        for (MTBackupDatacenterAddress *address in data.addressList) {
            MTSignal *signal = [self fetchConfigFromAddress:address currentContext:currentContext];
            if (delay > DBL_EPSILON) {
                signal = [signal delay:delay onQueue:[[MTQueue alloc] init]];
            }
            [signals addObject:signal];
            delay += 5.0;
        }
        return [[MTSignal mergeSignals:signals] take:1];
    };
}

现在,DC 2的身份验证信息已准备就绪,可以进行连接②,因为DC 2被编码为默认的主数据中心ID,所以将使用身份验证信息重新创建新的TCP连接④:

// Account.swift
public func accountWithId(accountManager: AccountManager, networkArguments: NetworkInitializationArguments, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, supplementary: Bool, rootPath: String, beginWithTestingEnvironment: Bool, backupData: AccountBackupData?, auxiliaryMethods: 
    ...
    return initializedNetwork(
        arguments: networkArguments, 
        supplementary: supplementary, 
        datacenterId: 2,  // use DC 2 for unauthrized account
        keychain: keychain, 
        basePath: path, 
        testingEnvironment: beginWithTestingEnvironment, 
        languageCode: localizationSettings?.primaryComponent.languageCode, 
        proxySettings: proxySettings, 
        networkSettings: networkSettings, phoneNumber: nil)
                        |> map { network -> AccountResult in
                            return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection))
                        }
}

同时,Google DoH返回196.55.216.85DC 4的新地址。对于任何新发现的端点,fetchConfigFromAddress调用requestDatacenterAddress以发送RPChelp.getConfig进行新配置。

作为一种新MTContext的不复制内部数据创建,它不知道原来的情况下可能有DC 4的身份验证信息,具有创造DC 4的身份验证信息的TCP连接⑤,断开连接,然后重新连接到通过另一个连接⑥发送RPC。

从DC 4返回的配置内部的数据中心地址被提取并解码为MTDatacenterAddressListData。然后关闭连接⑥。

// Addresses in Config.config from DC 4
MTDatacenterAddressListData({
    1 =     (
        "149.154.175.51:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.175.50:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23d:f001:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    2 =     (
        "149.154.167.50:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.167.51:443#(media no, cdn no, preferForProxy yes, secret )",
        "149.154.167.151:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f002:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f002:0000:0000:0000:000b:443#(media yes, cdn no, preferForProxy no, secret )"
    );
    3 =     (
        "149.154.175.100:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.175.100:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23d:f003:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    4 =     (
        "149.154.167.92:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.167.92:443#(media no, cdn no, preferForProxy yes, secret )",
        "149.154.165.96:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f004:0000:0000:0000:000b:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f004:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    5 =     (
        "91.108.56.143:443#(media no, cdn no, preferForProxy no, secret )",
        "91.108.56.143:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23f:f005:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
})

如果新数据不同,则原始上下文将使用新配置替换其种子数据中心地址集,并将其保存到钥匙串中。更改也分派给所有监听者。原文作者认为,代码内部存在一个错误fetchConfigFromAddresscurrentAddressSet是从错误的上下文中提取的,并且始终为 nil

// MTBackupAddressSignals.m, fetchConfigFromAddress
__strong MTContext *strongCurrentContext = weakCurrentContext;
[result.addressList enumerateKeysAndObjectsUsingBlock:^(NSNumber *nDatacenterId, NSArray *list, __unused BOOL *stop) {
  MTDatacenterAddressSet *addressSet = [[MTDatacenterAddressSet alloc] initWithAddressList:list];
  // Bug here, should use `strongCurrentContext` instead of `context`
  MTDatacenterAddressSet *currentAddressSet = [context addressSetForDatacenterWithId:[nDatacenterId integerValue]];
  // It's always true as `currentAddressSet` is always nil
  if (currentAddressSet == nil || ![addressSet isEqual:currentAddressSet])
  {
      [strongCurrentContext 
          updateAddressSetForDatacenterWithId:[nDatacenterId integerValue] 
                                   addressSet:addressSet 
                           forceUpdateSchemes:true];
      ...
  }
}];

// MTContext.m
- (void)updateAddressSetForDatacenterWithId:(NSInteger)datacenterId 
   addressSet:(MTDatacenterAddressSet *)addressSet 
   forceUpdateSchemes:(bool)updateSchemes {
   ...
   // replace the address set and save it the Keychain
   _datacenterAddressSetById[@(datacenterId)] = addressSet;
   [_keychain setObject:_datacenterAddressSetById 
                 forKey:@"datacenterAddressSetById" 
                  group:@"persistent"];
   ...
   bool shouldReset = previousAddressSetWasEmpty || updateSchemes;
   ...
   // broadcast the change event. `shouldReset` is True if the callee is fetchConfigFromAddress 
   for (id listener in currentListeners) {
       [listener 
           contextDatacenterTransportSchemesUpdated:self 
                                       datacenterId:datacenterId 
                                        shouldReset:shouldReset];
   }
}

// MTProto.m
- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context 
                                   datacenterId:(NSInteger)datacenterId 
                                    shouldReset:(bool)shouldReset {
   ...        
   if (resolvedShouldReset) {
       // reset the current transport
       [self resetTransport];
       [self requestTransportTransaction];
   }
   ...
}

监听者之一是MTProto,它保持与DC 2的有效连接④。它被命令重置其传输并创建与149.154.167.51DC 2的连接⑦ 。

到目前为止,还没有用户交互,让我们刷新状态:

创建了7个TCP连接和4个HTTP请求。与DC 2的连接⑦处于活动状态,而其他关闭。
客户端已经收集了DC 1、2、4的认证信息。
数据中心地址集已更新。

输入电话号码

part-4-phonenumber.png

输入电话号码并点击下一步后,auth.sendCode将通过活动连接将RPC发送到DC2。它将PHONE_MIGRATE_5作为帐户属于DC 5进行响应。

// Authorization.swift
public func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal {
   ...
   switch (error.errorDescription ?? "") {
       case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"):
           let range = error.errorDescription.range(of: "MIGRATE_")!
           // extract data center id from error description
           let updatedMasterDatacenterId = Int32(error.errorDescription[range.upperBound ..< error.errorDescription.endIndex])!
           let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: updatedMasterDatacenterId)
           return updatedAccount
           |> mapToSignalPromotingError { updatedAccount -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in
               return updatedAccount.network.request(sendCode, automaticFloodWait: false)
               |> map { sentCode in
                   return (sentCode, updatedAccount)
               }
           }
   }
   ...
}

// Account.swift, class Account
public func changedMasterDatacenterId(accountManager: AccountManager, masterDatacenterId: Int32) -> Signal {
   ...
   return accountManager.transaction { ... }
   |> mapToSignal { localizationSettings, proxySettings -> Signal<(LocalizationSettings?, ProxySettings?, NetworkSettings?), NoError> in
       ...
   }
   |> mapToSignal { (localizationSettings, proxySettings, networkSettings) -> Signal in
       return initializedNetwork(arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil)
       |> map { network in
           let updated = UnauthorizedAccount(networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network)
           updated.shouldBeServiceTaskMaster.set(self.shouldBeServiceTaskMaster.get())
           return updated
       }
   }
}

客户端被告知,DC 5是主数据中心,而不是DC 2.调用initializedNetwork并创建实例UnauthorizaedAccount来重发再次auth.sendAuth到DC 5的内部initializedNetwork,新MTContext创建以支持后续业务。

由于客户端还没有DC 5的身份验证信息,因此将建立连接⑧。同时,不幸的是,UnauthorizaedAccount新的实例启动了相同的端点发现逻辑,这导致了更多不必要的操作:

  • 它创建HTTP请求④⑤⑥⑦,并且DC 4再次获得了相同的地址196.55.216.85。
  • 由于fetchConfigFromAddress相同的复制问题,由于内部的MTContext无法识别DC 4的身份验证信息,因此会为其创建连接 ⑨ ,并创建 ⑨ 再次发送help.getConfig协议。
  • 在配置数据中返回相同的地址集。该currentAddressSet错误导致它调用updateAddressSetForDatacenterWithId。
  • 错误的更改事件将传播到与MTProtoDC 5具有有效连接⑧的。它将重置它并创建连接⑪以继续向DC 5发出身份验证信息请求。

获得身份验证信息后,连接⑪将关闭,并且连接 ⑫将开始发送auth.sendAuthto DC 5。

此阶段的摘要:

  • 创建5个TCP连接和4个HTTP请求。⑫到DC 5的连接and和DC到DC 2的连接被激活,而其他开关则关闭。实施的注意事项导致了多余的连接和请求。
  • 客户端获得DC 1、2、4、5的身份验证信息。
  • 地址集列表没有更改,尽管它已再次更新。

顺便说一句,如果种子地址和从DoH来的备用地址不起作用了,Telegram-iOS会在20秒后提示你要设置代理,目前没有其他解决办法。

输入授权码

part-4-code.png

输入从SMS接收到的登录代码后,auth.signIn通过活动连接由RPC发送。DC 5验证其正确并返回auth.Authorization.authorization。最终,客户端最终可以通过调用switchToAuthorizedAccount来替换其未经授权的状态。

创建新的AccountNetwork替换正在使用的,连接⑫和⑦关闭。创建连接 ⑬ 连接91.108.56.143到DC 5获取用户身份验证数据。ChatHistoryPreloadManager建立连接 ⑭ 与DC 5的连接以下载聊天记录。

managedConfigurationUpdates函数中一堆RPC通过连接⑬发送,包括help.getConfig协议。DC 5的配置与DC 4的配置具有不同的地址集列表,尤其是DC 5的地址更改为91.108.56.156。连接⑬⑭和不作为受到新的配置forceUpdateSchemes是假的在managedConfigurationUpdates

// Addresses in Config.config from DC 5
MTDatacenterAddressListData({
    1 =     (
        "149.154.175.57:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.175.50:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23d:f001:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    2 =     (
        "149.154.167.50:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.167.51:443#(media no, cdn no, preferForProxy yes, secret )",
        "149.154.167.151:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f002:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f002:0000:0000:0000:000b:443#(media yes, cdn no, preferForProxy no, secret )"
    );
    3 =     (
        "149.154.175.100:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.175.100:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23d:f003:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    4 =     (
        "149.154.167.92:443#(media no, cdn no, preferForProxy no, secret )",
        "149.154.167.92:443#(media no, cdn no, preferForProxy yes, secret )",
        "149.154.166.120:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f004:0000:0000:0000:000b:443#(media yes, cdn no, preferForProxy no, secret )",
        "2001:067c:04e8:f004:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
    5 =     (
        "91.108.56.156:443#(media no, cdn no, preferForProxy no, secret )",
        "91.108.56.156:443#(media no, cdn no, preferForProxy yes, secret )",
        "2001:0b28:f23f:f005:0000:0000:0000:000a:443#(media no, cdn no, preferForProxy no, secret )"
    );
})

主界面已准备好与帐户一起显示。其他模块开始获取UI组件的资源,例如头像图片等。所有请求均通过multiplexedRequestManager拥有的account.network发送。在此登录会话期间,所有资源都位于DC 1上。

在客户端可以从DC 1下载文件之前,它必须将其用户授权从DC 5转移到DC1。创建连接 ⑮要求DC 5导出身份验证数据,创建连接⑯将数据导入DC 1。业务完成后,两个连接均关闭。

MultiplexedRequestManagerContext每个DC最多限制4个workers。这就是为什么有连接 ⑰ ⑱ ⑲ ⑳用来Download的原因。

4 结论

  • 在首次登录过程中。尽管要完成许多任务,但拥有20个TCP连接和8个HTTP请求可能并不好。
  • 可以优化连接和请求的使用。
  • 为了使即使通讯产品可靠地连接到其数据中心,它需要很多效果。对于意外的网络问题,必须有多个备份计划。除了Telegram中使用的方法外,还有许多其他方法可以探索。
  • 我对以纯粹的反应方式对连接和数据的状态进行建模感到褒贬不一。好消息是它确实有效。但这会导致具有许多信号的复杂依赖关系结构。
  • 集成调试工具以帮助探索运行状态将非常好,例如Flipper,FLEX,Timelane等。

你可能感兴趣的:(Telegram-iOS 源码分析:第四部分(MTProto))