对 Telegram 协议和组织方法的批评。 第 1 部分,技术:从头开始编写客户端的经验 - TL、MT

最近,Habré 上出现的帖子越来越多,关于 Telegram 有多好,Durov 兄弟在构建网络系统方面有多么出色和经验等等。 与此同时,很少有人真正沉浸在技术设备中——他们最多使用一个相当简单(并且与 MTProto 有很大不同)的基于 JSON 的 Bot API,并且通常只接受 凭着信心 所有那些围绕信使的赞美和公关。 大约一年半前,我在 NPO Echelon Vasily 的同事(不幸的是,他在 Habré 上的帐户随草稿被删除)开始用 Perl 从头开始​​编写他自己的 Telegram 客户端,后来这些行的作者加入了。 为什么是 Perl,有些人会立即问? 因为其他语言已经有这样的项目了。其实这不是重点,其他任何语言都可以 成品库,因此作者必须一路走下去 从头开始. 此外,密码学是这样一种东西——信任,但要验证。 对于以安全为中心的产品,您不能只依赖供应商的现成库并盲目相信它(但是,这是第二部分的更多主题)。 目前,该库在“中间”级别上运行良好(允许您发出任何 API 请求)。

然而,这一系列的帖子中不会有太多密码学和数学方面的内容。 但是还会有许多其他技术细节和架构拐杖(它对于那些不会从头开始编写但会以任何语言使用该库的人也很有用)。 所以,主要目标是尝试从头开始实现客户端 根据官方文档. 也就是说,假设官方客户端的源代码是封闭的(同样,在第二部分中,我们将更详细地揭示这到底是什么的话题 那里 所以),但是,就像过去一样,例如,有一个像 RFC 这样的标准 - 是否可以单独根据规范编写客户端,“不偷看”源代码,甚至是官方的(Telegram Desktop,mobile) ,甚至是非官方的 Telethon?

目录:

文档……在吗? 是真的吗?...

这篇文章的笔记片段从去年夏天开始收集。 一直在官方网站上 https://core.telegram.org 文档是从第 23 层开始的,即停留在 2014 年的某个地方(记住,那时甚至还没有频道?)。 当然,理论上,这应该可以在2014年实现一个具有当时功能的客户端。 但即使在这种情况下,文档首先是不完整的,其次,有些地方自相矛盾。 一个多月前,也就是 2019 年 XNUMX 月, 偶然 我们发现该站点对文档进行了大幅更新,更新了第 105 层,并指出现在所有内容都需要重新阅读。 的确,很多文章都修改过,但也有很多没变。 因此,在阅读下面关于文档的批评时,您应该记住,其中一些内容不再相关,但有些内容仍然相关。 毕竟,5 年在现代世界不仅很多,而且 很多。 从那时起(特别是如果你不考虑从那时起丢弃和复活的地理聊天),该方案中的 API 方法数量从一百个增加到两百五十多个!

作为一名年轻作家,你从哪里开始?

无论您是从头开始编写还是使用例如现成的库都没有关系 Python 电视马拉松 или 玛德琳 PHP,无论如何,你首先需要 注册您的应用程序 - 获取参数 api_id и api_hash (使用过 VKontakte API 的人会立即明白)服务器将通过它来识别应用程序。 这 必须 出于法律原因,但我们将在第二部分详细讨论为什么图书馆作者不能发布它。 也许您会对测试值感到满意,尽管它们非常有限 - 事实上,现在您可以注册您的号码 只有一个 申请,所以不要急于求成。

现在,从技术的角度来看,我们应该对这样一个事实感兴趣,即在注册后我们应该从 Telegram 收到有关文档、协议等更新的通知。 也就是说,可以假设带有码头的站点只是简单地“得分”并继续专门与那些开始吸引客户的人合作,因为。 这更容易。 但是没有,没有观察到那样的情况,没有任何信息传来。

而如果你从头开始写,那么接收到的参数的使用其实还很遥远。 虽然 https://core.telegram.org/ 并首先在入门中讨论它们,实际上,您首先必须实施 MTProto协议 - 但如果你相信 根据 OSI 模型布局 在协议的一般描述页面的末尾,然后完全是徒劳的。

事实上,无论是在 MTProto 之前还是之后,同时在多个级别上(正如从事 OS 内核工作的外国网络人员所说,层违规),一个大的、痛苦的和可怕的话题将阻碍......

二进制序列化:TL(Type Language)和它的scheme,还有layers,还有很多吓人的词

事实上,这个主题是 Telegram 问题的关键。 如果你试图深入研究它,将会有很多可怕的词。

所以,计划。 如果你记得这个词,说, JSON 架构你想对了。 目标是一样的:用某种语言来描述一组可能的传输数据。 事实上,这就是相似性结束的地方。 如果从页面 MTProto协议,或者从官方客户端的源码树,我们尝试打开一些scheme,我们会看到类似这样的东西:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

第一次看到这个的人会凭直觉只认出写的部分——好吧,这些显然是结构(尽管名字在哪里,左边还是右边?),里面有字段,之后该类型通过冒号......可能。 在这里,在尖括号中,可能有 C++ 中的模板(实际上, 不太好). 所有其他符号是什么意思,问号,感叹号,百分比,格子(显然它们在不同地方表示不同的东西),出现在某个地方,但不在某个地方,十六进制数字 - 最重要的是,如何从中得到 右边 (哪个不会被服务器拒绝)字节流? 你必须阅读文档 (是的,附近有指向 JSON 版本中的模式的链接——但这并不能使它更清楚).

打开页面 二进制数据序列化 并沉浸在蘑菇和离散数学的神奇世界中,类似于第 4 年的 matan。 字母表、类型、值、组合子、函数式组合子、范式、复合类型、多态类型……这只是第一页! 接下来等你 天语,虽然它已经包含了一个简单的请求和响应的例子,但根本没有提供对更典型案例的答案,这意味着你将不得不在八个以上的嵌套上费力地复述从俄语翻译成英语的数学页!

熟悉函数式语言和自动类型推断的读者,当然看到这种语言的描述,即使是从一个例子来看,也熟悉得多,可以说这在原理上大体上是不错的。 对此的反对意见是:

  • 是的, 目的 听起来不错,可惜 没有达到
  • 俄罗斯大学的教育甚至因 IT 专业而异 - 并非每个人都读过相应的课程
  • 最后,正如我们将看到的,在实践中它是 它不需要,因为仅使用了所描述的 TL 的有限子集

在话 狮子座 在频道上 #perl 在 FreeNode IRC 网络上,尝试实现从 Telegram 到 Matrix 的门(根据记忆,引用的翻译不准确):

感觉就像第一次接触类型理论的人很兴奋并开始尝试使用它,而不是真正关心在实践中是否有必要。

自己看看是否需要裸类型(int、long 等)作为基本的东西不会引起问题 - 最后它们必须手动实现 - 例如,让我们尝试从它们派生 向量. 也就是说,事实上, 排列,如果你用它们的专有名称来调用结果。

但是之前

TL 语法子集的简要说明,供不了解的人阅读官方文档

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

总是开始定义 设计师,之后,可选地(在实践中,总是)通过符号 # 应该 CRC32 来自给定类型的规范化描述字符串。 接下来是字段的描述,如果它们是 - 类型可以为空。 这一切都以等号结尾,即给定构造函数(实际上是子类型)所属类型的名称。 等号右边的类型是 多态的 ——也就是可以对应几种具体的类型。

如果定义出现在该行之后 ---functions---,那么语法将保持不变,但含义将不同:构造函数将成为 RPC 函数的名称,字段将成为参数(好吧,也就是说,它将保持完全相同的给定结构,如下所述,它只是给定的含义),并且“多态类型”是返回结果的类型。 是的,它仍将保持多态性 - 仅在部分中定义 ---types---,并且不会考虑此构造函数。 通过参数输入被调用函数的重载,即出于某种原因,TL 中没有提供几个与 C++ 中相同名称但不同签名的函数。

如果不是 OOP,为什么要使用“构造函数”和“多态”? 好吧,事实上,有人从 OOP 的角度来考虑它会更容易——一个多态类型作为一个抽象类,构造函数是它的直接后代类,而且 final 在许多语言的术语中。 其实当然在这里 相似 在 OO 编程语言中使用真正重载的构造函数方法。 由于这里只有数据结构,没有方法(尽管下面对函数和方法的描述很容易在头脑中产生关于它们是什么的混淆,但这是关于其他东西的)——你可以把构造函数想象成从中获得价值 正在建设中 读取字节流时输入。

这是怎么发生的? 始终读取 4 个字节的解串器看到值 0xcrc32 - 并了解接下来会发生什么 field1 与类型 int, IE。 恰好读取 4 个字节,在这个具有类型的覆盖字段上 PolymorType 读。 看见 0x2crc32 并了解到还有两个领域,首先 long,所以我们读取了 8 个字节。 然后又是一个复杂类型,它以相同的方式反序列化。 例如, Type3 只要两个构造函数分别进一步必须满足其中一个,就可以在模式中声明 0x12abcd34, 之后你需要再读 4 个字节 int0x6789cdef,之后什么都没有。 任何其他 - 你需要抛出异常。 无论如何,之后我们返回读取 4 个字节 int 领域 field_c в constructorTwo 然后我们读完我们的 PolymorType.

最后,如果被抓住 0xdeadcrcconstructorThree,然后事情变得更加复杂。 我们的第一个领域 bit_flags_of_what_really_present 与类型 # - 事实上,这只是类型的别名 nat意为“自然数”。 也就是说,事实上,unsigned int 是唯一的情况,顺便说一句,在实际电路中发现无符号数。 因此,接下来是一个带问号的构造,这意味着这是字段 - 只有在引用的字段中设置了相应的位(大约像三元运算符)时,它才会出现在线路上。 所以,假设这个位是打开的,那么你需要读取一个字段,比如 Type,在我们的示例中有 2 个构造函数。 一个是空的(只包含一个标识符),另一个有字段 ids 与类型 ids:Vector<long>.

你可能会认为模板和泛型都很好,或者 Java。 但不是。 几乎。 这 唯一的 实际电路中尖括号的情况,它仅用于 Vector。 在字节流中,对于 Vector 类型本身,这将是 4 个 CRC32 字节,始终相同,然后是 4 个字节 - 数组元素的数量,然后是这些元素本身。

除此之外,序列化总是以 4 字节的字出现,所有类型都是它的倍数 - 还描述了内置类型 bytes и string 手动序列化长度和对齐 4 - 好吧,这听起来很正常,甚至相对有效? 虽然 TL 号称是高效的二进制序列化,但见鬼去吧,随着任何东西的扩展,甚至布尔值和最大 4 字节的单字符字符串,JSON 还会厚很多吗? 看,即使不需要的字段也可以通过位标志跳过,一切都很好,甚至可以为将来扩展,你后来是否在构造函数中添加了新的可选字段?..

但是不,如果你不是阅读我的简短描述,而是阅读完整的文档,并考虑实施。 首先,构造函数的 CRC32 是通过规范化的模式文本描述字符串计算的(删除多余的空格等)——因此,如果添加了新字段,类型描述字符串将发生变化,因此它的 CRC32 和序列化也会发生变化。 如果老客户收到一个设置了新标志的字段,但他不知道接下来要做什么,他会怎么做?..

其次,让我们记住 CRC32, 在这里基本上用作 散列函数 唯一确定正在(反)序列化的类型。 在这里,我们面临碰撞的问题 - 不,概率不是 232 分之一,而是更多。 谁记得 CRC32 旨在检测(和纠正)通信通道中的错误,并相应地改进这些属性以损害他人的利益? 例如,她不关心字节的排列:如果你从两行计算 CRC32,在第二行你将交换前 4 个字节和接下来的 4 个字节——它们将是相同的。 当我们将来自拉丁字母表的文本字符串(和一些标点符号)作为输入,并且这些名称不是特别随机时,这种排列的概率就会大大增加。

顺便说一句,谁检查了那里的东西 CRC32? 在早期资料之一(甚至在 Waltman 之前)中有一个哈希函数,可以将每个字符乘以数字 239,深受这些人的喜爱,哈哈!

最后,好吧,我们意识到具有字段类型的构造函数 Vector<int> и Vector<PolymorType> 会有不同的CRC32。 线上的演示呢? 而在理论上, 它是否成为类型的一部分? 假设我们传递了一个包含一万个数字的数组,那么, Vector<int> 一切都很清楚,长度和另外 40000 个字节。 如果这个 Vector<Type2>,它只包含一个字段 int 它是该类型中唯一的一个 - 我们是否需要重复 10000xabcdef0 34 次然后是 4 个字节 int,或者语言能够从构造函数中为我们显示这个 fixedVec 而不是 80000 个字节,只传输 40000 个字节?

这根本不是一个无意义的理论问题——想象一下你得到一个组用户列表,每个用户都有一个 ID、名字、姓氏——通过移动连接传输的数据量的差异可能很大。 向我们宣传的是 Telegram 序列化的有效性。

所以...

无法推导的向量

如果您尝试仔细阅读组合子的描述页面,您会看到一个向量(甚至矩阵)正试图通过元组推导出几张表。 但最后他们被锤了,最后一步被跳过,简单地给出了一个向量的定义,它也没有绑定到一个类型。 这是怎么回事? 在语言中 程序设计,尤其是函数式的,递归地描述结构是很典型的——具有惰性评估的编译器将理解一切并做到这一点。 在语言中 数据序列化 但需要效率:简单描述就足够了 , IE。 两个元素的结构 - 第一个是数据元素,第二个是相同的结构本身或尾部的空白空间(包 (cons) 在 Lisp 中)。 但这显然需要 元素额外花费 4 个字节(在 TL 的情况下为 CRC32)来描述其类型。 数组很容易描述 固定大小,但在先前未知长度的数组的情况下,我们中断。

因此,由于 TL 不允许您输出向量,因此必须在旁边添加它。 最终文档说:

序列化始终使用不依赖于类型 t 变量的特定值的相同构造函数“向量”(const 0x1cb5c415 = crc32(“vector t:Type # [ t ] = Vector t”)。

可选参数 t 的值不参与序列化,因为它是从结果类型派生的(在反序列化之前总是已知的)。

细看: vector {t:Type} # [ t ] = Vector t - 但 无处 定义本身并没有说第一个数字必须等于向量的长度! 它不会从任何地方跟随。 这是给定的,您需要牢记并亲自实施。 在其他地方,文档甚至诚实地提到该类型是假的:

Vector t 多态伪类型是一种“类型”,其值是任意类型 t 的值序列,可以是 boxed 或 bare。

......但不专注于此。 当你厌倦了数学的延伸(你甚至可能从大学课程中知道),决定评分并观察如何在实践中实际使用它时,印象会留在你的脑海中:这里的严肃数学是基于,显然是 Cool People(两位数学家 - ACM 的获胜者),而不仅仅是任何人。 目标 - 挥霍 - 已经实现。

顺便说一下,关于数量。 记起 # 它是同义词 nat, 自然数:

有类型表达式 (类型表达式) 和数值表达式 (自然表达式). 但是,它们的定义方式相同。

type-expr ::= expr
nat-expr ::= expr

但在语法中,它们以相同的方式描述,即必须再次记住这种差异并手动投入实施。

嗯,是的,模板类型(vector<int>, vector<User>) 有一个共同的标识符 (#1cb5c415), IE。 如果您知道调用被声明为

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

那么你等待的不仅仅是一个向量,而是一个用户向量。 更确切地说, должен 等等 - 在实际代码中,每个元素,如果不是裸类型,都会有一个构造函数,并且在实现中以一种好的方式有必要检查 - 我们被准确地发送到这个向量的每个元素中 那种? 如果它是某种 PHP,其中数组可以包含不同元素中的不同类型?

此时,你开始怀疑——需要这样的 TL 吗? 也许对于购物车来说,可以使用人工序列化器,即当时已经存在的同一个 protobuf? 这是理论,让我们看看实践。

代码中现有的 TL 实现

TL 诞生于 VKontakte 的内部,甚至在出售 Durov 的股份和(一定),甚至在 Telegram 开发之前。 并且在开源中 第一次实施的来源 你可以找到很多有趣的拐杖。 而且语言本身在那里的实现比现在在 Telegram 中的实现更全面。 例如,该方案中根本没有使用哈希(意味着具有异常行为的内置伪类型(如向量))。 或者

Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

但为了完整起见,让我们考虑一下这幅图画,以便追溯,可以这么说,思想巨人的演变。

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

或者这个美丽的:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

这个片段是关于模板的,比如:

intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

这是 hashmap 模板类型的定义,作为 int 类型对的向量。 在 C++ 中,它看起来像这样:

    template <T> class IntHash {
      vector<pair<int,T>> _map;
    }

所以在这里 alpha - 关键字! 但是只有在C++中你可以写T,但是你必须写alpha,beta……但是不能超过8个参数,幻想在theta上结束了。 因此,似乎在圣彼得堡曾经有过这样的对话:

-- Надо сделать в TL шаблоны
-- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
-- Грамматика? Ну потом напишем

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

但这是关于“一般”的 TL 的第一个布局实施。 让我们继续考虑实际 Telegram 客户端中的实现。

罗勒的话:

Vasily,[09.10.18 17:07] 最重要的是,他们搞砸了一堆抽象,然后他们在上面敲了一个螺栓,并用拐杖覆盖了 codegegerator
结果,首先从码头 pilot.jpg
然后从 jekichan.webp 代码

当然,对于熟悉算法和数学的人来说,我们可以预期他们读过 Aho、Ullman,并且熟悉这些工具,这些工具已经成为几十年来业界为他们的 DSL 编写编译器的事实标准,对吧? ..

由作者 电报客户端 是 Vitaliy Valtman,从 TLO 格式超出其 (cli) 限制的出现可以理解,团队成员 - 现在分配了用于解析 TL 的库 对她的印象如何 TL解析器?..

16.12 04:18 Vasily:在我看来,有人没有掌握 lex + yacc
16.12 04:18 Vasily:不然我没法解释
16.12 04:18 Vasily:好吧,或者他们为 VK 中的行数付费
16.12 04:19 Vasily:其他 3k+ 行<censored> 而不是解析器

也许是个例外? 让我们看看如何 品牌 这是官方客户端 — Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

Python 中的 1100 多行,一些正则表达式 + 向量类型的特殊情况,当然,根据 TL 语法在方案中声明它应该是这样的,但是他们把它放在这个语法上,更多地解析它... 问题是,为什么要为所有这些奇迹而烦恼и更多的粉扑,如果没有人会根据文档解析它?!

顺便说一下...还记得我们讨论过 CRC32 校验吗? 因此,在 Telegram Desktop 代码生成器中,有一个例外列表,用于计算 CRC32 的那些类型 不匹配 如图所示!

Vasily,[18.12 22:49] 在这里你应该考虑是否需要这样的 TL
如果我想搞乱替代实现,我会开始插入换行符,一半的解析器会中断多行定义
但是,tdesktop 也是

记住关于单行的要点,我们稍后会回到它。

好的,telegram-cli 是非官方的,Telegram Desktop 是官方的,但是其他的呢? 谁知道呢?.. 在 Android 客户端代码中,根本没有模式解析器(这引发了关于开源的问题,但这是第二部分),但还有其他几个有趣的代码片段,但关于它们下面的小节。

序列化在实践中还提出了哪些其他问题? 例如,他们当然搞砸了位字段和条件字段:

轻率地: flags.0? true
表示该字段存在并且如果设置了标志则为真

轻率地: flags.1? int
表示该字段存在,需要反序列化

瓦西里:屁股,别烧了,你在干什么!
Vasily:文档中某处提到 true 是零长度的裸类型,但从他们的文档中收集一些东西是不现实的
Vasily:在开放实现中也没有这样的东西,但是有很多拐杖和道具

电视马拉松怎么样? 展望 MTProto 的主题,一个例子 - 文档中有这样的部分,但标志 % 它仅被描述为“对应于给定的裸类型”,即在下面的示例中,要么是错误,要么是未记录的内容:

瓦西里,[22.06.18/18/38 XNUMX:XNUMX] 在一个地方:

msg_container#73f1f8dc messages:vector message = MessageContainer;

在不同的:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

这是两个很大的区别,在现实生活中,出现了某种裸向量

我还没有看到裸矢量定义,也没有遇到过它

手写在电视节目中的分析

他的模式注释掉了定义 msg_container

同样,问题仍然是 about%。 它没有被描述。

Vadim Goncharov,[22.06.18/19/22 XNUMX:XNUMX PM] 在 tdesktop 中?

Vasily,[22.06.18/19/23 XNUMX:XNUMX] 但是他们在监管机构上的 TL 解析器可能也不会吃掉它

// parsed manually

TL 是一个美丽的抽象,没有人完全实现它

他们的计划版本中没有 %

但是这里的文档自相矛盾,所以 xs

它是在语法中发现的,他们可能只是忘记描述语义

好吧,您在 TL 上看到了码头,没有半升就无法弄清楚

“好吧,比方说,”另一位读者会说,“你批评一切,所以就应该表现出来。”

Vasily 回答:“至于解析器,我需要像

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

不知何故比

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

или

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

这是整个词法分析器:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

那些。 更简单的是委婉地说。”

一般来说,最终,实际使用的 TL 子集的解析器和代码生成器适合大约 100 行语法和 ~ 300 行生成器(包括所有 print的生成代码),包括类型好东西,每个类中用于自省的类型信息。 每个多态类型都变成一个空的抽象基类,构造函数继承自它并具有序列化和反序列化的方法。

类型语言中缺少类型

强类型很好,对吧? 不,这不是 holivar(尽管我更喜欢动态语言),而是 TL 中的假设。 基于它,语言应该为我们提供各种检查。 好吧,好吧,不是让他,而是实施,但他至少应该描述一下。 我们想要什么机会?

首先,约束。 这里我们在上传文件的文档中看到:

然后文件的二进制内容被分成几部分。 所有零件必须具有相同的尺寸( 零件尺寸 ) 并且必须满足以下条件:

  • part_size % 1024 = 0 (能被1KB整除)
  • 524288 % part_size = 0 (512KB 必须能被 part_size 整除)

最后一部分不必满足这些条件,只要它的大小小于 part_size。

每个部分都应该有一个序号, 文件部分, 取值范围为 0 到 2,999。

文件分区后,您需要选择一种方法将其保存在服务器上。 使用 上传.saveBigFilePart 如果文件的完整大小超过 10 MB 并且 上传.saveFilePart 对于较小的文件。
[…] 可能会返回以下数据输入错误之一:

  • FILE_PARTS_INVALID - 零件数量无效。 该值不介于 1..3000

模式中是否存在这些? 它是否可以通过 TL 以某种方式表达? 不。 但是对不起,即使是老式的 Turbo Pascal 也能够描述由 范围. 他还可以做一件事,现在更广为人知的是 enum - 由固定(少量)值的枚举组成的类型。 请注意,在 C 之类的语言中 - 数字,到目前为止我们只讨论了类型。 数字. 但是也有数组,字符串……比如,描述这个字符串只能包含一个电话号码就好了,对吧?

这些都不在 TL 中。 但是,例如,在 JSON Schema 中。 如果其他人可以反对 512 KB 的可分割性,这仍然需要在代码中检查,那么请确保客户端简单地 不能 发送超出范围的号码 1..3000 (并且不会出现相应的错误)这是可能的,对吧?..

顺便说一下错误和返回值。 即使对于那些与 TL 合作过的人来说,眼睛也模糊了——我们并没有立即意识到这一点 TL 中的函数实际上不仅可以返回所描述的返回类型,还可以返回错误。 但这不能通过 TL 本身推导出来。 当然,这是可以理解的,nafig 在实践中是不必要的(虽然实际上,RPC 可以通过不同的方式来完成,我们会回到这个)——但是来自天堂世界的抽象类型数学概念的纯度呢? .. 抓住了拖船 - 所以匹配。

最后,可读性如何? 好吧,总的来说,我想 描述 让它在模式中正确(同样,它在 JSON 模式中),但如果它已经用它紧张,那么实际方面呢 - 至少在更新期间观察差异是老生常谈? 亲自看看 真实的例子:

- channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

или

- message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

有人喜欢它,但是 GitHub,例如,拒绝在这么长的行中突出显示更改。 游戏“找10个不同点”,大脑第一眼看到的是两个例子的开头和结尾是一样的,你需要在中间的某个地方繁琐地阅读......在我看来,这不仅仅是理论上的,但纯粹从视觉上看 又脏又乱.

顺便说一句,关于理论的纯度。 为什么需要位域? 他们似乎不 从类型论的角度来看不好吗? 在架构的早期版本中可以看到解释。 起初,是的,是这样的,每次打喷嚏都会产生一种新的类型。 这些雏形仍然以这种形式存在,例如:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

但是现在想象一下,如果您的结构中有 5 个可选字段,那么所有可能的选项都需要 32 种类型。 组合爆炸。 因此,TL理论的水晶纯度再次撞上了连载的严酷现实的铸铁屁股。

此外,在某些地方,这些人自己也违反了自己的打字规定。 例如,在 MTProto(下一章)中,响应可以通过 Gzip 压缩,一切都是合理的——除了违反层和模式。 有一次,并没有收获 RpcResult 本身,而是收获了它的内容。 好吧,为什么要这样做?..我不得不砍掉一根拐杖,这样压缩就可以在任何地方发挥作用。

或者另一个例子,我们曾经发现一个错误——发送 InputPeerUser 而不是 InputUser. 或相反亦然。 但它奏效了! 也就是说,服务器不关心类型。 怎么会这样? 答案可能会由来自 telegram-cli 的代码片段提示:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

也就是说,到这里序列化就完成了 手册,不是生成的代码! 也许服务器是以类似的方式实现的?.. 原则上,如果完成一次就可以工作,但是以后如何通过更新来支持它? 这不是计划的目的吗? 然后我们继续下一个问题。

版本控制。 图层

为什么模式版本被称为层只能根据已发布模式的历史来猜测。 显然,起初在作者看来,基本的事情可以在一个不变的方案上完成,并且只有在必要时,才向特定请求表明它们是根据不同的版本完成的。 原则上,即使是一个好主意 - 新的也会“混合”在旧的之上。 但让我们看看它是如何完成的。 没错,从一开始就不可能看到 - 这很有趣,但基础层方案根本不存在。 层从 2 开始。文档告诉我们一个特殊的 TL 功能:

如果客户端支持第 2 层,则必须使用以下构造函数:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

实际上,这意味着在每次 API 调用之前,一个具有值的 int 0x289dd1f6 必须在方法编号之前添加。

听起来不错。 但接下来发生了什么? 然后来了

invokeWithLayer3#b7475268 query:!X = X;

那么下一步是什么? 因为很容易猜到

invokeWithLayer4#dea0d430 query:!X = X;

有趣的? 不,现在笑还早,想想什么 来自另一层的请求需要包装在这样一个特殊的类型中——如果它们都不同,还能如何区分它们? 在前面仅添加 4 个字节是一种非常有效的方法。 所以

invokeWithLayer5#417a57ae query:!X = X;

但很明显,过一段时间就会变成一些酒神。 解决方案来了:

更新:从第 9 层开始,辅助方法 invokeWithLayerN 可以一起使用 initConnection

万岁! 在 9 个版本之后,我们终于来到了 80 年代互联网协议所做的事情——在连接开始时进行一次版本协商!

那么接下来呢?...

invokeWithLayer10#39620c41 query:!X = X;
...
invokeWithLayer18#1c900537 query:!X = X;

现在你可以笑了。 又过了9层,终于加了一个带版本号的通用构造函数,只需要在连接开始时调用一次,层中的意思好像没了,现在只是一个条件版本,比如其他地方。 问题解决了。

正确的?..

瓦西里,[16.07.18/14/01 XNUMX:XNUMX PM] 星期五我想:
远程服务器无需请求即可发送事件。 请求需要包装在 InvokeWithLayer 中。 服务器不包装更新,没有包装响应和更新的结构。

那些。 客户端无法指定他想要更新的层

Vadim Goncharov,[16.07.18/14/02 XNUMX:XNUMX PM] InvokeWithLayer 原则上不是拐杖吗?

瓦西里,[16.07.18/14/02 XNUMX:XNUMX PM] 这是唯一的方法

Vadim Goncharov,[16.07.18/14/02 XNUMX:XNUMX PM] 本质上应该意味着在会议开始时分层

顺便说一句,由此可见,不提供客户端降级

更新,即类型 Updates 在该方案中,这是服务器发送给客户端的内容,不是响应 API 请求,而是在事件发生时自行发送。 这是一个复杂的话题,将在另一篇文章中讨论,但现在重要的是要知道服务器即使在客户端离线时也会累积更新。

因此,当拒绝包装时 包来指示其版本,因此逻辑上可能会出现以下问题:

  • 服务器在客户端告知它支持哪个版本之前向客户端发送更新
  • 升级客户端后应该做什么?
  • гарантирует服务器对层数的看法不会在此过程中改变?

你认为这纯粹是理论上的想法,而实际上这不可能发生,因为服务器写得正确(无论如何,它被测试得很好)? 哈! 不管怎样!

这正是我们在 14 月遇到的情况。 XNUMX 月 XNUMX 日,消息闪现,Telegram 服务器正在更新某些内容……然后在日志中:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

然后是几兆字节的堆栈跟踪(好吧,与此同时,日志记录已修复)。 毕竟,如果你的 TL 中没有识别出某些东西 - 它是二进制的签名,在流中更远 ALL 去,解码将变得不可能。 遇到这种情况怎么办?

好吧,任何人想到的第一件事就是断开连接并重试。 没有帮助。 我们用谷歌搜索了 CRC32——这些结果是方案 73 中的对象,尽管我们在方案 82 上工作。我们仔细查看日志——有来自两个不同方案的标识符!

也许问题纯粹出在我们的非官方客户端? 不,我们运行 Telegram Desktop 1.2.17(许多 Linux 发行版提供的版本),它写入异常日志:MTP Unexpected type id #b5223b0f read in MTPMessageMedia ...

对 Telegram 协议和组织方法的批评。 第 1 部分,技术:从头开始编写客户端的经验 - TL、MT

谷歌表明,一个非官方客户已经发生了类似的问题,但是版本号和相应的假设是不同的......

那么该怎么办? Vasily和我分道扬镳:他试过把scheme更新到91,我决定等几天试试73。两种方法都管用,但由于是经验,不知道你需要跳多少个版本或下来,也不是你要等多久。

后来,我设法重现了这种情况:我们启动客户端,将其关闭,将方案重新编译到另一层,重新启动,再次发现问题,返回到之前的 - 糟糕,没有切换方案并重新启动客户端几次分钟会有所帮助。 您将收到来自不同层的混合数据结构。

解释? 正如您可以从各种间接症状中猜测的那样,服务器由不同机器上的许多不同类型的进程组成。 最有可能的是,负责“缓冲”的服务器之一将较高级服务器提供给它的内容放入队列中,并按照生成时的方案提供。 在这个队列“腐烂”之前,我们无能为力。

除非……但这是一个可怕的拐杖?!……不,在思考疯狂的想法之前,让我们先看看官方客户端的代码。 在 Android 版本中,我们没有找到任何 TL 解析器,但是我们找到了一个带有(反)序列化的大文件(github 拒绝给它着色)。 以下是代码片段:

public static class TL_message_layer68 extends TL_message {
    public static int constructor = 0xc09be45f;
//...
//еще пачка подобных
//...
    public static class TL_message_layer47 extends TL_message {
        public static int constructor = 0xc992e15c;
        public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            Message result = null;
            switch (constructor) {
                case 0x1d86f70e:
                    result = new TL_messageService_old2();
                    break;
                case 0xa7ab1991:
                    result = new TL_message_old3();
                    break;
                case 0xc3060325:
                    result = new TL_message_old4();
                    break;
                case 0x555555fa:
                    result = new TL_message_secret();
                    break;
                case 0x555555f9:
                    result = new TL_message_secret_layer72();
                    break;
                case 0x90dddc11:
                    result = new TL_message_layer72();
                    break;
                case 0xc09be45f:
                    result = new TL_message_layer68();
                    break;
                case 0xc992e15c:
                    result = new TL_message_layer47();
                    break;
                case 0x5ba66c13:
                    result = new TL_message_old7();
                    break;
                case 0xc06b9607:
                    result = new TL_messageService_layer48();
                    break;
                case 0x83e5de54:
                    result = new TL_messageEmpty();
                    break;
                case 0x2bebfa86:
                    result = new TL_message_old6();
                    break;
                case 0x44f9b43d:
                    result = new TL_message_layer104();
                    break;
                case 0x1c9b1027:
                    result = new TL_message_layer104_2();
                    break;
                case 0xa367e716:
                    result = new TL_messageForwarded_old2(); //custom
                    break;
                case 0x5f46804:
                    result = new TL_messageForwarded_old(); //custom
                    break;
                case 0x567699b3:
                    result = new TL_message_old2(); //custom
                    break;
                case 0x9f8d60bb:
                    result = new TL_messageService_old(); //custom
                    break;
                case 0x22eb6aba:
                    result = new TL_message_old(); //custom
                    break;
                case 0x555555F8:
                    result = new TL_message_secret_old(); //custom
                    break;
                case 0x9789dac4:
                    result = new TL_message_layer104_3();
                    break;

или

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

嗯……看起来很疯狂。 但是,大概,这是一个生成的代码,那好吗?..但它肯定支持所有版本! 没错,目前尚不清楚为什么所有东西都混合在一堆,秘密聊天以及各种 _old7 不知何故与机器生成不相似......然而,最重要的是我从

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

伙计们,你们连一层都不能决定吗?! 好吧,好吧,比方说,“两个”被错误地释放了,好吧,它发生了,但是三个?.. 立即再次在同一个耙子上? 抱歉,这是什么色情内容?...

顺便说一下,类似的事情发生在 Telegram Desktop 源代码中——如果是这样的话,并且连续多次提交到该方案并没有改变它的层数,而是修复了一些东西。 在方案没有官方数据来源的情况下,除了官方客户来源外,我可以从哪里获得数据? 从那里开始,在测试所有方法之前,您无法确定该方案是否完全正确。

这怎么能被测试呢? 希望单元测试、功能测试和其他测试的爱好者在评论中分享。

好,我们再看一段代码:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

这里的“手动创建”注释表明该文件只有一部分是手写的(您能想象维护的噩梦吗?),其余部分是机器生成的。 然而,另一个问题出现了——资源是可用的 不是完全的 (Linux 内核中 GPL 下的 la blob),但这已经是第二部分的主题了。

但足够了。 让我们继续讨论所有这些序列化所遵循的协议。

MT 原型

所以让我们打开 一般描述 и 协议的详细描述 我们绊倒的第一件事是术语。 并拥有丰富的一切。 一般来说,这似乎是 Telegram 的商标——以不同的方式在不同的地方调用事物,或者用一个词调用不同的事物,反之亦然(例如,在高级 API 中,如果你看到一个贴纸包——这个不是你想的那样)。

例如,“消息”(message) 和“会话”(session)——在这里它们的含义与 Telegram 客户端通常的界面不同。 好吧,消息的一切都很清楚,可以用 OOP 来解释,或者简称为“包”这个词——这是一个低级的传输层,没有与界面中相同的消息,有很多服务的。 但是会议……但要事第一。

传输层

首先是交通。 我们将被告知 5 个选项:

  • TCP
  • 网络套接字
  • 通过 HTTPS 的网络套接字
  • HTTP
  • HTTPS

Vasily,[15.06.18/15/04 XNUMX:XNUMX PM] 还有 UDP 传输,但没有记录

和 TCP 的三种变体

第一种类似于 UDP over TCP,每个数据包都包含一个序列号和一个 crc
为什么在推车上阅读码头如此痛苦?

现在好了 TCP 已有 4 种变体:

  • 简略
  • 中级
  • 填充中间体

好的,填充了 MTProxy 的中间体,这是后来由于已知事件而添加的。 但是,为什么还有两个版本(总共三个),而一个可以做到呢? 这四个本质上的区别仅在于如何设置主 MTProto 本身的长度和有效负载,这将在后面进一步讨论:

  • 在 Abridged 中它是 1 或 4 个字节但不是 0xef 那么 body
  • 在 Intermediate 中,这是 4 个字节的长度和一个字段,客户端第一次必须发送 0xeeeeeeee 表明它是中级
  • 从网络工作者的角度来看,完整版是最容易上瘾的:长度、序列号,而不是基本上是 MTProto、正文、CRC32 的那个。 是的,所有这些都通过 TCP。 它以串行字节流的形式为我们提供了可靠的传输,不需要序列,尤其是校验和。 好的,现在他们会反对我说 TCP 有一个 16 位校验和,所以数据损坏发生了。 很好,除了我们实际上有一个哈希值超过 16 字节的加密协议,所有这些错误 - 甚至更多 - 都将在更高级别的 SHA 不匹配上被捕获。 CRC32 对此毫无意义。

让我们比较一下 Abridged,其中一个字节的长度是可能的,Intermediate 证明“以防需要 4 字节数据对齐”,这是非常无稽之谈。 什么,据信 Telegram 程序员笨到无法将数据从套接字读取到对齐的缓冲区中? 您仍然必须这样做,因为读取可以返回任意数量的字节(并且还有代理服务器,例如......)。 或者,另一方面,如果顶部的 16 个字节仍有大量填充,为什么还要使用 Abridged - 节省 3 个字节 有时 ?

给人的印象是,尼古拉杜罗夫非常喜欢发明自行车,包括网络协议,但没有真正的实际需要。

其他交通选择,包括。 Web 和 MTProxy,我们现在不考虑,如果有要求,可能在另一篇文章中考虑。 我们现在只会回忆起这个 MTProxy,它在 2018 年发布后不久,供应商很快学会了完全阻止它,旨在 块旁路通过 数据包大小! 还有一个事实,即 MTProxy 服务器(同样由 Waltman)用 C 编写,尽管根本不需要(Phil Kulin 会确认),但与 Linux 细节不必要地联系在一起,而且在 Go 或 Node.js 上也有类似的服务器适合不到一百行。

但在考虑其他问题后,我们将在本节末尾对这些人的技术素养得出结论。 现在,让我们继续讨论第 5 个 OSI 层,会话——他们在上面放置了 MTProto 会话。

密钥、消息、会话、Diffie-Hellman

他们把它放在那里并不完全正确......会话不是活动会话下界面中可见的会话。 但秩序井然。

对 Telegram 协议和组织方法的批评。 第 1 部分,技术:从头开始编写客户端的经验 - TL、MT

在这里,我们从传输层接收到一串已知长度的字节。 这要么是加密消息,要么是明文——如果我们仍处于密钥协商阶段并且实际上正在这样做的话。 我们在谈论哪些称为“密钥”的概念? 让我们为 Telegram 团队本身澄清这个问题(我很抱歉将我自己的文档从英语翻译成早上 4 点的大脑疲倦,保留一些短语更容易):

有两个实体叫做 会议 - 一个在“当前会话”下的官方客户端用户界面中,其中每个会话对应于整个设备/操作系统。
第二个 - MTProto会话,其中有一个消息序列号(在低级意义上),并且 可能在不同的 TCP 连接之间持续。 可以同时设置多个 MTProto 会话,例如,以加快文件下载速度。

在这两者之间 招生面试 是概念 授权. 在退化的情况下,可以说 界面会话 是相同的 授权但是,唉,这很复杂。 我们看:

  • 新设备上的用户首先生成 授权键 并将其绑定到帐户,例如,通过 SMS - 这就是为什么 授权
  • 它发生在第一个 MTProto会话, 其中有 session_id 在你自己里面。
  • 在这一步,组合 授权 и session_id 可以命名 - 这个词出现在一些客户的文档和代码中
  • 然后,客户端可以打开 一些 MTProto 会话 在同一个 授权键 - 到同一个 DC。
  • 然后有一天客户需要从 另一个DC - 对于这个 DC,将生成一个新的 授权键 !
  • 告诉系统这不是新用户注册,而是同一个 授权 (界面会话), 客户端使用 API 调用 auth.exportAuthorization 在家用直流 auth.importAuthorization 在新的DC中。
  • 都一样,可能有几个开 MTProto 会话 (每个都有自己的 session_id) 到这个新的 DC,在 他的 授权键.
  • 最后,客户可能想要完全前向保密。 每一个 授权键永久 密钥 - 每个 DC - 客户端可以调用 auth.bindTempAuthKey 用来 临时 授权键 - 再一次,只有一个 临时验证密钥 每个 DC,对所有人通用 MTProto 会话 到这个DC。

需要注意的是 (和未来的盐)也是一个 授权键 那些。 所有人共享 MTProto 会话 到同一个DC。

“在不同的 TCP 连接之间”是什么意思? 这意味着这个 就像是 网站上的授权 cookie - 它持续(存活)到该服务器的许多 TCP 连接,但总有一天它会变坏。 与 HTTP 不同的是,在 MTProto 中,在会话内部,消息按顺序编号和确认,它们进入隧道,连接断开——在建立新连接后,服务器将在会话中发送它在会话中未传递的所有内容以前的 TCP 连接。

然而,上述信息是经过数月的诉讼后才得到的。 与此同时,我们是否从头开始实施我们的客户? -让我们回到开头。

所以我们生成 auth_key来自 Telegram 的 Diffie-Hellman 版本. 让我们试着理解文档......

Vasily,[19.06.18/20/05 1:255] data_with_hash := SHAXNUMX(data) + data + (any random bytes); 这样长度等于XNUMX字节;
encrypted_data := RSA(data_with_hash, server_public_key); 一个 255 字节长的数字(大端)被提高到必要模数的必要幂,结果存储为 256 字节数字。

他们得到了一些毒品 DH

看起来不像健康人的DH
dx 中没有两个公钥

好吧,最后,我们弄明白了,但沉积物仍然存在——客户完成了工作证明,证明他能够对数字进行因式分解。 针对 DoS 攻击的保护类型。 而RSA密钥只在一个方向上使用一次,本质上是为了加密 new_nonce. 但是,当这个看似简单的操作成功时,您将不得不面对什么?

Vasily,[20.06.18/00/26 XNUMX:XNUMX] 我还没有达到 appid 请求

我向 DH 发送了请求

并且,在运输工具的码头上写着它可以用 4 个字节的错误代码来回答。 就是这样

好吧,他告诉我-404,那又怎样?

我对他说:“抓住你的 efigna 用服务器密钥加密,我想要 DH”,它愚蠢地响应 404

您会如何看待这样的服务器响应? 该怎么办? 没有人要问(但在第二部分中有更多内容)。

这里所有对dock的兴趣都在做

无事可做,只梦想着把数字来回转换

两个 32 位数字。 我像其他人一样打包它们

但是不,你首先需要的是这两个作为 BE

Vadim Goncharov,[20.06.18/15/49 404:XNUMX PM] 因为这个 XNUMX?

瓦西里,[20.06.18/15/49 XNUMX:XNUMX PM] 是的!

Vadim Goncharov,[20.06.18/15/50 XNUMX:XNUMX PM] 所以我不明白他能“找不到”什么

瓦西里,[20.06.18 15:50] 关于

我没有找到这样的分解成简单的除数)

连报错都没掌握

Vasily,[20.06.18/20/18 5:XNUMX PM] 哦,还有 MDXNUMX。 已经有三个不同的哈希值

密钥指纹计算如下:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 和 sha2

所以让我们把 auth_key 根据 Diffie-Hellman,我们得到了 2048 位的大小。 下一步是什么? 然后我们发现这个密钥的低 1024 位没有以任何方式使用......但是让我们现在考虑一下。 在这一步,我们与服务器有一个共享的秘密。 已经建立了一个 TLS 会话的模拟,这是一个非常昂贵的过程。 但是服务器还不知道我们是谁! 还没有,实际上 授权书. 那些。 如果您考虑的是“登录密码”,就像它曾经在 ICQ 中一样,或者至少是“登录密钥”,就像在 SSH 中一样(例如,在某些 gitlab / github 上)。 我们得到了匿名。 如果服务器回答我们“这些电话号码由另一个 DC 提供服务”? 甚至“您的电话号码被禁止”? 我们能做的最好的事情就是保存密钥,希望到那时它仍然有用并且不会腐烂。

顺便说一句,我们有保留地“收到”它。 例如,我们信任服务器吗? 他是假的吗? 我们需要密码检查:

Vasily,[21.06.18/17/53 2:XNUMX PM] 为了简单起见,他们提供移动客户端来检查 XNUMXkbit 数字)

但是一点都不清楚,nafeijoa

Vasily, [21.06.18/18/02 XNUMX:XNUMX] 码头没有说如果结果不简单怎么办

没说。 让我们看看Android 的官方客户端在这种情况下做了什么? A 那是什么 (是的,整个文件在那里都很有趣)——正如他们所说,我就把它留在这里:

278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
279   if (!strcasecmp(prime, goodPrime)) {

不,当然有 一些 有检查数字的简单性,但就我个人而言,我不再拥有足够的数学知识。

好的,我们得到了万能钥匙。 登录,即发送请求,有必要执行进一步的加密,已经使用 AES。

消息密钥定义为消息正文(包括会话、消息 ID 等)的 SHA128 的中间 256 位,包括填充字节,前面加上取自授权密钥的 32 个字节。

瓦西里,[22.06.18/14/08 XNUMX:XNUMX PM] 平均母狗

有一个 auth_key. 全部。 此外,他们……从码头上还不清楚。 随意研究开源代码。

请注意,MTProto 2.0 需要 12 到 1024 字节的填充,但仍需满足生成的消息长度可被 16 字节整除的条件。

那么要放入多少填充物?

是的,这里也是 404,以防出现错误

如果有人仔细研究了图表和文档的文本,他会注意到那里没有 MAC。 并且该 AES 在其他任何地方都没有使用的某些 IGE 模式中使用。 当然,他们在他们的常见问题解答中写下了它......在这里,消息密钥本身同时是用于检查完整性的解密数据的 SHA 哈希 - 如果不匹配,文档出于某种原因建议默默地忽略它们(但是安全性如何,突然破坏我们?)。

我不是密码学家,也许在这种情况下,从理论上的角度来看,在这种模式下并没有错。 但我绝对可以使用 Telegram Desktop 的示例来命名一个实际问题。 它以与 MTProto 中的消息(仅在本例中为 877 版)相同的方式加密本地缓存(所有这些 D783F5D3D8EF1.0C),即首先是消息密钥,然后是数据本身(以及主要的大 auth_key 256 字节,没有它 msg_key 无用)。 因此,这个问题在大文件上变得很明显。 即,您需要保留数据的两个副本 - 加密和解密。 如果有兆字节或流式视频,例如?..密文后带有 MAC 的经典方案允许您读取流式传输,并立即传输。 使用 MTProto 你必须 起初 加密或解密整个消息,然后才将其传输到网络或磁盘。 因此,在最新版本的 Telegram Desktop 中的缓存中 user_data 另一种格式已经被使用 - 在 CTR 模式下使用 AES。

Vasily,[21.06.18/01/27 20:XNUMX AM] 哦,我发现了 IGE 是什么:IGE 是“验证加密模式”的首次尝试,最初是针对 Kerberos。 这是一次失败的尝试(它不提供完整性保护),必须被删除。 这是 XNUMX 年来寻求有效的身份验证加密模式的开始,最近以 OCB 和 GCM 等模式达到顶峰。

现在来自购物车方面的论点:

Telegram 背后的团队由 Nikolai Durov 领导,由六名 ACM 冠军组成,其中一半是数学博士。 他们花了大约两年的时间才推出当前版本的 MTProto。

有什么好笑的。 两年到较低水平

或者我们可以只接受 tls

好的,假设我们已经完成了加密和其他细微差别。 我们最终可以发送 TL 序列化请求并反序列化响应吗? 那么应该发送什么以及如何发送? 这是方法 初始化连接也许就是这样?

Vasily,[25.06.18/18/46 XNUMX:XNUMX PM] 初始化连接并保存有关用户设备和应用程序的信息。

它接受 app_id、device_model、system_version、app_version 和 lang_code。

还有一些查询

一如既往的文档。 随意研究开源

如果使用 invokeWithLayer 一切都大致清楚,那么它是什么? 事实证明,假设我们有——客户端已经有一些东西要询问服务器——有一个我们想要发送的请求:

Vasily, [25.06.18/19/13 XNUMX:XNUMX] 从代码来看,第一个调用被包装在这个垃圾中,垃圾本身在 invokewithlayer

为什么 initConnection 不能是一个单独的调用,而必须是一个包装器? 是的,事实证明,它必须在每次会话开始时每次都完成,而不是像主键那样一次性完成。 但! 未经授权的用户不能调用它! 在这里我们已经到了适用的阶段 这一个 文档页面 - 它告诉我们......

只有一小部分 API 方法可供未经授权的用户使用:

  • 验证发送代码
  • auth.重发代码
  • account.get密码
  • 验证.checkPassword
  • auth.check电话
  • 授权注册
  • 授权登录
  • auth.import授权
  • 帮助.getConfig
  • 帮助.getNearestDc
  • 帮助.getAppUpdate
  • 帮助.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.get差异
  • langpack.getLanguages
  • langpack.get语言

他们中的第一个 auth.sendCode,还有那个珍贵的第一个请求,我们将在其中发送 api_id 和 api_hash,然后我们会收到一条带有代码的短信。 如果我们到达了错误的 DC(例如,这个国家的电话号码由另一个国家提供服务),那么我们将收到一个错误消息,其中包含所需 DC 的号码。 要通过 DC 号码找出我们需要连接到哪个 IP 地址,我们将帮助 help.getConfig. 曾经只有5个参赛作品,但在经历了2018年的知名事件后,数量大幅增加。

现在让我们记住我们在匿名服务器上的这个阶段。 仅仅获得一个IP地址会不会太贵了? 为什么不在 MTProto 的未加密部分执行此操作和其他操作? 我听到一个反对意见:“你怎么能确定不是 RKN 会用假地址回应?”。 对此我们记得,事实上,在官方客户中 嵌入式 RSA 密钥, IE。 你可以 订阅 此信息。 实际上,关于绕过客户端通过其他渠道收到的锁的信息,这已经完成了(这是合乎逻辑的,这不能在 MTProto 本身中完成,因为您仍然需要知道连接到哪里)。

好的。 在客户授权的这个阶段,我们还没有被授权,也没有注册我们的申请。 我们现在只想看看服务器对未授权用户可用的方法有何响应。 和这里…

瓦西里,[10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

在计划中,第一个,第二个来了

在 tdesktop 模式中,第三个值是

是的,从那时起,当然,文档已经更新。 尽管很快它可能会再次变得无关紧要。 新手开发人员应该如何知道? 也许如果您注册您的应用程序,他们会通知您? Vasily 这样做了,但是很遗憾,没有任何东西发送给他(同样,我们将在第二部分中讨论这个)。

...您注意到我们已经以某种方式转移到 API,即到下一个级别并错过了 MTProto 主题中的某些内容? 没什么奇怪的:

Vasily,[28.06.18/02/04 2:XNUMX AM] 嗯,他们正在翻找 eXNUMXe 上的一些算法

Mtproto 为两个域定义了加密算法和密钥,以及一些包装器结构

但是他们不断地混合不同的堆栈级别,所以并不总是很清楚 mtproto 在哪里结束以及下一个级别从哪里开始。

它们是如何混合的? 好吧,这里是 PFS 的相同临时密钥,例如(顺便说一下,Telegram Desktop 不知道该怎么做)。 它由 API 请求执行 auth.bindTempAuthKey, IE。 从顶层。 但与此同时,它会干扰较低级别的加密 - 例如,在它之后,您需要再次进行 initConnection 等等,这不是 只是 正常要求。 另外,它还规定您在 DC 上只能拥有一个临时密钥,尽管字段 auth_key_id 在每条消息中至少允许您更改每条消息的密钥,并且服务器有权随时“忘记”临时密钥 - 在这种情况下该怎么办,文档没有说明......好吧,为什么不可能有多个密钥,就像一组未来的盐一样,但是?..

MTProto 主题中还有其他一些值得注意的事情。

消息消息、msg_id、msg_seqno、确认、错误方向的 ping 和其他特性

为什么您需要了解它们? 因为它们“泄漏”了更高一层,您在使用 API 时需要了解它们。 假设我们对 msg_key 不感兴趣,下层为我们解密了一切。 但是在解密后的数据里面,我们有以下字段(也是数据的长度,可以知道padding在哪里,不过这个不重要):

  • 盐-int64
  • session_id - int64
  • message_id - int64
  • seq_no-int32

回想一下,整个 DC 只有一种盐。 为什么会知道她? 不仅因为有要求 get_future_salts,它告诉哪些间隔是有效的,但也因为如果你的盐是“烂的”,那么消息(请求)就会丢失。 服务器当然会通过发出来报告新盐 new_session_created - 但是对于旧的,您将不得不以某种方式重新发送,例如。 这个问题会影响应用程序的架构。

出于多种原因,允许服务器完全丢弃会话并以这种方式响应。 实际上,客户端的 MTProto 会话是什么? 这是两个数字 session_id и seq_no 此会话中的消息。 好吧,当然还有底层的 TCP 连接。 假设我们的客户仍然不知道如何做很多事情,断开连接,重新连接。 如果这发生得很快——旧会话在新的 TCP 连接中继续,增加 seq_no 更远。 如果花费很长时间,服务器可能会删除它,因为正如我们发现的那样,它在它的一侧也是一个队列。

应该是什么 seq_no? 哦,这是一个棘手的问题。 尝试诚实地理解其含义:

内容相关消息

需要明确确认的消息。 这些包括所有用户和许多服务消息,几乎所有的容器和确认除外。

消息序列号 (msg_seqno)

一个 32 位数字,等于发送者在此消息之前创建的“内容相关”消息(需要确认的消息,特别是那些不是容器的消息)的两倍,如果当前消息是内容相关的消息。 容器总是在其全部内容之后生成; 因此,它的序号大于或等于其中包含的消息的序号。

这是什么马戏团,增量为 1,然后又是 2?..我怀疑原来的意思是“低位为 ACK,其余为数字”,但结果不太正确 - 特别是,原来可以发 一些 具有相同的确认 seq_no! 如何? 好吧,比如服务器给我们发了东西,发了,我们自己就沉默了,我们只回复服务确认信息,关于收到他的信息。 在这种情况下,我们的传出确认将具有相同的传出号码。 如果你熟悉 TCP 并认为这听起来有点疯狂,但它似乎不是很疯狂,因为在 TCP seq_no 没有改变,确认去 seq_no 对方——那我赶紧不高兴。 MTProto 即将收到确认 seq_no,就像在 TCP 中一样,但是 msg_id !

这是什么 msg_id,这些领域中最重要的? 顾名思义,消息的唯一 ID。 它被定义为一个 64 位数字,其中最低有效位再次具有服务器非服务器的魔力,其余是 Unix 时间戳,包括小数部分,向左移动 32 位。 那些。 timestamp 本身(并且时间差异太大的消息将被服务器拒绝)。 从这里可以看出,一般来说,这是一个对客户端来说是全局的标识符。 虽然 - 记住 session_id - 我们保证: 在任何情况下都不能将用于一个会话的消息发送到另一个会话. 也就是原来已经有 级别——会话、会话号、消息 ID。 为什么这么复杂,这个谜团很大。

因此, msg_id 需要……

RPC:请求、响应、错误。 确认。

您可能已经注意到,虽然有答案,但模式中的任何地方都没有特殊类型或函数“发出 RPC 请求”。 毕竟,我们有内容相关的消息! 那是, 任何 留言可以是请求! 或者不是。 毕竟, msg_id. 以下是答案:

rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

这是指示这是对哪条消息的响应的地方。 因此,在 API 的顶层,你将不得不记住你的请求有多少 - 我认为没有必要解释工作是异步的,并且可以同时有多个请求,回答哪个可以按任意顺序退货吗? 原则上,从这个,以及类似 no worker 的错误消息,可以追踪到这背后的架构:与你保持 TCP 连接的服务器是一个前端平衡器,它将请求定向到后端并收集它们回来 message_id. 这里的一切似乎都清晰、合乎逻辑且良好。

是吗?.. 如果你考虑一下? 毕竟,RPC 响应本身也有一个字段 msg_id! 我们是否需要对服务器大喊“你没有回应我的回答!”? 是的,确认有什么用? 关于页面 关于消息的消息 告诉我们什么是

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

每一方都必须这样做。 但不总是! 如果您收到 RpcResult,它本身就是一种确认。 也就是说,服务器可以用 MsgsAck 响应您的请求——比如“我收到了”。 可以立即回答 RpcResult。 可能两者都是。

是的,您仍然必须回答答案! 确认。 否则服务器会认为未送达,再次丢给你。 即使在重新连接之后。 但是这里当然会出现超时问题。 让我们稍后再看。

同时,让我们考虑查询执行中可能出现的错误。

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

哦,有人会惊呼,这是一种更人性化的格式——有一条线! 慢慢来。 这里 错误列表但肯定不完整。 从中我们了解到代码是 - 就像是 HTTP 错误(嗯,当然,响应的语义没有得到尊重,在某些地方它们是由代码随机分布的),字符串看起来像 CAPITAL_LETTERS_AND_NUMBERS 大写. 例如,PHONE_NUMBER_OCCUPIED 或 FILE_PART_X_MISSING。 好吧,就是你还是要这条线 解析。 例如, FLOOD_WAIT_3600 将意味着你必须等待一个小时,并且 PHONE_MIGRATE_5具有此前缀的电话号码应在 5th DC 中注册。 我们有一种类型语言,对吗? 我们不需要字符串中的参数,正则表达式就可以了,cho。

同样,这不在服务消息页面上,但是,正如这个项目已经习惯的那样,可以找到信息 在另一个文档页面上。 或 引起怀疑. 首先,看,违反打字/层 - RpcError 可以投资 RpcResult. 为什么不在外面? 我们没有考虑到什么?..因此,保证在哪里 RpcError 不能投资 RpcResult,而是直接或嵌套在另一种类型中? 它缺乏 req_msg_id ?..

但是让我们继续服务消息。 客户端可能认为服务端想了很久,提出了这么奇葩的请求:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

它有三种可能的答案,再次与确认机制相交,试图理解它们应该是什么(以及一般不需要确认的类型列表是什么),留给读者作为作业(注意: Telegram 桌面资源中的信息不完整)。

成瘾:消息发布状态

总的来说,TL、MTProto、Telegram很多地方总的来说给人一种固执的感觉,但出于礼貌、圆滑等 软技能 我们礼貌地对此保持沉默,对话中的脏话被审查了。 然而,这个地方О大部分关于页面 关于消息的消息 连我这个长期接触网络协议,见识过各种弯曲度数的自行车的人都感到震惊。

它以无害的方式开始,需要确认。 接下来,我们被告知

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

好吧,每个开始使用 MTProto 的人都将不得不面对它们,在“更正 - 重新编译 - 启动”循环中,在编辑过程中出现数字错误或盐分变质是很常见的事情。 但是,这里有两点:

  1. 由此可见,原始消息丢失了。 我们需要隔离一些队列,我们​​稍后会考虑这个。
  2. 那些奇怪的错误号是什么? 16、17、18、19、20、32、33、34、35、48、64……汤米,剩下的数字在哪里?

文件指出:

意图是将error_code值分组(error_code >> 4):例如代码0x40 - 0x4f对应容器分解中的错误。

但是,首先,向另一个方向转变,其次,其余代码在哪里并不重要? 在作者的脑海中?.. 但是,这些都是小事。

上瘾始于发布状态消息和发布副本:

  • 消息状态信息请求
    如果任何一方在一段时间内没有收到关于其传出消息状态的信息,它可以明确地向另一方请求:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • 有关消息状态的信息性消息
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    在这里, info 是一个字符串,它包含传入 msg_ids 列表中每条消息的消息状态的一个字节:

    • 1 = 消息一无所知(msg_id 太低,对方可能忘记了)
    • 2 = message not received(msg_id在存储的标识符范围内,但对方肯定没有收到过这样的消息)
    • 3 = message not received(msg_id太高,但是对方肯定还没有收到)
    • 4 = 消息已收到(请注意,此响应同时也是接收确认)
    • +8 = 消息已经确认
    • +16 = 消息不需要确认
    • +32 = 正在处理或处理已完成的消息中包含的 RPC 查询
    • +64 = 对消息的内容相关响应已经生成
    • +128 = 对方知道消息已经收到
      此响应不需要确认。 它本身就是对相关 msgs_state_req 的确认。
      请注意,如果突然发现对方没有看起来像是已发送给它的消息,则可以简单地重新发送该消息。 即使对方同时收到消息的两份副本,副本也会被忽略。 (如果时间过长,原来的 msg_id 不再有效,消息将被包装在 msg_copy 中)。
  • 消息状态的自愿通信
    任何一方都可以自愿将另一方发送的消息的状态告知另一方。
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • 一条消息状态的扩展自愿通信
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • 明确请求重新发送消息
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    远程方通过重新发送请求的消息立即响应 [...]
  • 明确要求重新发送答案
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    远程方立即通过重新发送来响应 答案 请求的消息 […]
  • 邮件副本
    在某些情况下,需要重新发送 msg_id 不再有效的旧消息。 然后,它被包裹在一个副本容器中:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    一旦接收到消息,就好像包装器不存在一样进行处理。 但是,如果确定收到消息 orig_message.msg_id,则不会处理新消息(同时确认它和 orig_message.msg_id)。 orig_message.msg_id 的值必须小于容器的 msg_id。

让我们甚至对以下事实保持沉默 msgs_state_info 再次,未完成的 TL 的耳朵伸出来(我们需要一个字节向量,在枚举的低两位中,在较旧的位标志中)。 重点是另一回事。 有谁知道为什么这一切在实践中 在真实客户端 有必要吗?.. 很难,但是你可以想象如果一个人在交互模式下进行调试会有一些好处 - 问服务器是什么以及如何。 但是这里描述了请求 双向.

由此可见,每一方不仅必须加密和发送消息,还必须存储关于它们的数据、关于它们的答案,并且存储时间未知。 该文档未描述这些功能的时间或实际适用性。 以任何方式. 最令人惊讶的是,它们竟然被用在了官方客户端的代码中! 显然,他们被告知了一些未包含在公开文档中的内容。 从代码中理解 为什么,不再像 TL 那样简单——这不是(相对)逻辑上孤立的部分,而是与应用程序架构相关的部分,即将需要更多时间来理解应用程序代码。

ping 和计时。 队列。

从所有方面来看,如果您还记得关于服务器架构的猜测(跨后端的请求分布),接下来会发生一件相当乏味的事情 - 尽管 TCP 中的所有交付保证(数据已经交付,或者您将被告知中断,但数据会一直传送到问题出现的那一刻),即 MTProto 本身的确认 - 没有保证. 服务器很容易丢失或丢弃您的消息,对此无能为力,只能围堵各种类型的拐杖。

首先是消息队列。 好吧,一方面,一切从一开始就很明显 - 必须存储并重新发送未经确认的消息。 什么时候之后? 小丑认识他。 或许那些瘾君子服务消息以某种方式用拐杖解决了这个问题,比如说,在 Telegram Desktop 中大约有 4 个队列与之对应(可能更多,如前所述,为此你需要更认真地研究它的代码和架构;同时时间,我们知道它不能作为样本,MTProto 方案中的一定数量的类型没有在其中使用)。

为什么会这样? 可能是服务器程序员无法确保集群内的可靠性,或者至少无法在前端平衡器上进行缓冲,并将此问题转移到客户端。 出于绝望,Vasily 尝试实现一个替代选项,只有两个队列,使用 TCP 算法 - 测量到服务器的 RTT 并根据未确认请求的数量调整“窗口”大小(在消息中)。 也就是说,这种用于估计服务器负载的粗略启发式方法 - 它可以同时处理我们的多少请求而不丢失。

嗯,也就是说,你明白了吧? 如果您必须在基于 TCP 的协议之上再次实现 TCP,则表明协议设计非常糟糕。

哦,是的,为什么需要多个队列,一般来说,这对于使用高级 API 的人来说意味着什么? 你看,你提出一个请求,你序列化它,但通常不可能立即发送。 为什么? 因为答案会是 msg_id, 这是暂时的а我是一个厂牌,约会最好推迟到越晚越好——突然服务器会因为我们和它的时间不匹配而拒绝它(当然,我们可以做一个拐杖,把我们的时间从现在转移通过添加从服务器响应计算的增量到服务器时间 - 官方客户端这样做,但由于缓冲,这种方法是粗糙和不准确的)。 因此,当您使用库中的本地函数调用发出请求时,消息会经历以下阶段:

  1. 位于同一个队列中,正在等待加密。
  2. 任命 msg_id 并且消息进入另一个队列 - 可能转发; 发送到套接字。
  3. a) 服务器回复 MsgsAck - 消息已发送,我们将其从“其他队列”中删除。
    b) 反之亦然,他不喜欢某事,他回答了 badmsg - 我们从“其他队列”重新发送
    c) 什么都不知道,有必要从另一个队列重新发送消息——但不知道确切的时间。
  4. 服务器终于回答了 RpcResult - 实际响应(或错误)- 不仅已交付,而且已处理。

也许,使用容器可以部分解决这个问题。 这是当一堆消息被打包成一个时,服务器立即响应所有消息,一个 msg_id. 但他也会拒绝这个包,万一出了什么差错,也是整件事情。

在这一点上,非技术因素开始发挥作用。 从经验来看,我们已经看到了很多拐杖,此外,现在我们将看到更多糟糕的建议和架构的例子——在这种情况下,是否值得信任并做出这样的决定? 这个问题是反问的(当然不是)。

我们在说啥啊? 如果在“关于消息的沉迷消息”这个话题上,你仍然可以用“你是愚蠢的,你不明白我们的好主意!”之类的反对意见来推测。 (所以先写文档,正常人应该,有基本原理和数据包交换示例,然后我们再说),那么计时/超时是一个纯粹实际和具体的问题,这里早就知道了。 但是文档告诉我们关于超时的内容是什么?

服务器通常使用 RPC 响应确认收到来自客户端的消息(通常是 RPC 查询)。 如果响应很长时间才到来,服务器可能首先发送一个接收确认,稍后再发送 RPC 响应本身。

客户端通常通过向下一个 RPC 查询添加确认来确认收到来自服务器的消息(通常是 RPC 响应),如果它没有传输得太晚(如果它是在接收后 60-120 秒生成的)来自服务器的消息)。 但是,如果在很长一段时间内都没有理由向服务器发送消息,或者如果有大量来自服务器的未确认消息(例如,超过 16 个),客户端将发送一个独立的确认。

... 我翻译:我们自己不知道需要多少和多少,好吧,让我们估计一下就这样吧。

关于 ping:

Ping 消息 (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

响应通常返回到相同的连接:

pong#347773c5 msg_id:long ping_id:long = Pong;

这些消息不需要确认。 pong 仅在响应 ping 时传输,而 ping 可以由任何一方发起。

延迟连接关闭 + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

像 ping 一样工作。 此外,在收到此消息后,服务器启动一个计时器,该计时器将在 disconnect_delay 秒后关闭当前连接,除非它收到一条自动重置所有先前计时器的相同类型的新消息。 例如,如果客户端每 60 秒发送一次这些 ping,它可以将 disconnect_delay 设置为 75 秒。

你是不是疯了?! 60秒后,列车将进站、上下客,再次在隧道中失联。 在 120 秒内,当你四处寻找时,他会到达另一个,并且连接很可能会断开。 好吧,很明显腿是从哪里长出来的——“我听到了铃声,但我不知道它在哪里”,有 Nagle 算法和 TCP_NODELAY 选项,它是为交互工作而设计的。 但是,对不起,延迟它的默认值 - 200 米利秒。 如果你真的想描绘类似的东西并保存在一对可能的数据包上 - 好吧,推迟至少 5 秒,或者任何消息“用户正在输入......”的超时现在等于。 但没有了。

最后,ping。 也就是说,检查 TCP 连接的活跃度。 这很有趣,但大约 10 年前,我写了一篇关于我们教员宿舍信使的批评性文本 - 作者还从客户端对服务器进行了 ping 操作,反之亦然。 但是三年级学生是一回事,国际办公室又是另一回事,对吧?..

首先,一个小型的教育计划。 在没有数据包交换的情况下,TCP 连接可以存在数周。 这有好有坏,取决于目的。 好吧,如果你打开了与服务器的 SSH 连接,你从计算机上站起来,重新启动电源路由器,返回到你的位置 - 通过该服务器的会话没有中断(没有输入任何内容,没有数据包),方便的。 如果服务器上有数千个客户端,每个客户端都会占用资源(你好 Postgres!),这很糟糕,并且客户端主机可能很久以前就重新启动了——但我们不会知道。

聊天/IM 系统属于第二种情况,还有一个额外的原因——在线状态。 如果用户“摔倒”,则有必要将其告知对话者。 否则,将会出现 Jabber 的创建者所犯的错误(并纠正了 20 年)——用户断开连接,但他们继续向他写消息,认为他在线(这在之前的几分钟内也完全丢失了)破口被发现)。 不,TCP_KEEPALIVE 选项,许多不了解 TCP 计时器如何工作的人,在任何地方弹出(通过设置野生值,如几十秒),在这里无济于事 - 你需要确保不仅是操作系统内核用户的机器还活着,但也能正常运行,能够回答,以及应用程序本身(你认为它不能冻结吗?Ubuntu 18.04 上的 Telegram Desktop 已经多次崩溃)。

这就是为什么你应该 ping 服务器 客户端,反之亦然 - 如果客户端这样做,当连接断开时,ping 将不会被传递,目标就不会实现。

我们在电报中看到了什么? 一切恰恰相反! 好吧,即当然,正式地,双方可以互相ping通。 在实践中,客户使用拐杖 ping_delay_disconnect,它会在服务器上启动一个计时器。 好吧,对不起,决定他想在没有 ping 的情况下在那里住多久不是客户的事。 服务器根据其负载了解得更多。 但是,当然,如果您不为资源感到难过,那么它们本身就是邪恶的木偶奇遇记,拐杖会掉下来...

它应该如何设计?

我相信上述事实非常清楚地表明 Telegram / VKontakte 团队在计算机网络传输(和较低)级别领域的能力不是很高,并且他们在相关事务上的资质也很低。

为什么它会变得如此复杂,Telegram 架构师又该如何反对呢? 事实上,他们试图建立一个在 TCP 连接中断后仍然存在的会话,也就是说,我们现在没有提供的内容,我们稍后会提供。 他们可能还尝试过进行 UDP 传输,尽管他们遇到了困难并放弃了它(这就是为什么文档是空的——没有什么值得吹嘘的)。 但是由于缺乏对一般网络和特定 TCP 工作原理的理解,您可以在哪里依赖它,以及您需要自己做(以及如何)的地方,并试图将其与密码学结合起来“一次两次”一块石头的鸟”-结果是这样的尸体。

应该如何? 基于这样的事实 msg_id 是防止重放攻击的加密必要时间戳,将唯一标识符函数附加到它是错误的。 因此,在不彻底改变当前架构的情况下(当更新线程形成时,这是本系列文章另一部分的高级 API 主题),必须:

  1. 保持与客户端的 TCP 连接的服务器负责 - 如果您从套接字中减去,请确认、处理或返回错误,没有损失。 然后确认不是 id 的向量,而只是“最后收到的 seq_no”——只是一个数字,就像在 TCP 中一样(两个数字——你自己的 seq 和确认)。 我们一直在开会,不是吗?
  2. 防止重放攻击的时间戳成为一个单独的字段,la nonce。 检查,但没有其他影响。 足够和 uint32 - 如果我们的 salt 至少每半天更改一次,我们可以将 16 位分配给当前时间整数部分的低位,其余部分分配给秒的小数部分(就像现在一样)。
  3. 缩回 msg_id 完全-从后端区分请求的角度来看,首先是客户端ID,其次是会话ID,然后将它们连接起来。 相应地,作为请求标识,一个就够了 seq_no.

也不是最好的选择,一个完整的随机数可以作为一个标识符——顺便说一下,这在发送消息时已经在高级 API 中完成了。 最好完全将架构从相对更改为绝对,但这是另一部分的主题,而不是这篇文章。

应用程序接口?

哒哒哒! 因此,在经历了充满痛苦和拐杖的道路之后,我们终于能够向服务器发送任何请求并接收对它们的任何答复,以及从服务器接收更新(不是响应请求,而是它发送给我们自己,例如 PUSH,如果有人更清楚的话)。

注意,现在文章中将出现唯一的 Perl 示例! (对于那些不熟悉语法的人,bless 的第一个参数是对象的数据结构,第二个是它的类):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

是的,特别是不剧透——如果你还没有读过,那就去做吧!

哦,哇~~......它看起来像什么? 一些非常熟悉的东西……也许这是 JSON 中典型 Web API 的数据结构,除了类可能附加到对象之外?..

事实证明......这是什么,同志们?..付出了很多努力 - 我们停下来休息 Web 程序员的地方 刚刚开始?.. JSON over HTTPS 会不会更简单?! 我们换来了什么? 这些努力值得吗?

让我们评估一下 TL+MTProto 为我们提供了什么以及可能的替代方案。 好吧,HTTP 请求-响应不太合适,但至少是在 TLS 之上?

紧凑的序列化。 看到这个数据结构,类似JSON,想起来还有它的二进制变体。 让我们将 MsgPack 标记为可扩展性不足,但是有,例如,CBOR - 顺便说一下,标准中描述的 RFC 7049. 值得注意的是它定义了 标签,作为一种扩展机制,并且在 已经标准化 可用:

  • 25 + 256 - 用行号引用替换重复行,这种廉价的压缩方法
  • 26 - 带有类名和构造函数参数的序列化 Perl 对象
  • 27 - 具有类型名称和构造函数参数的序列化语言无关对象

好吧,我尝试在启用字符串和对象打包的情况下在 TL 和 CBOR 中序列化相同的数据。 结果开始从 XNUMX 兆字节开始有利于 CBOR:

cborlen=1039673 tl_len=1095092

因此, 产量: 有很多更简单的格式,它们不会出现同步失败或未知标识符问题,而且效率相当。

快速连接建立. 这意味着重新连接后的 RTT 为零(当密钥已经生成一次时)——适用于第一个 MTProto 消息,但有一些保留——他们进入了相同的盐,会话没有变质,等等。 TLS 为我们提供了什么回报? 相关报价:

在 TLS 中使用 PFS 时,TLS 会话票证 (RFC 5077) 在不重新协商密钥且不在服务器上存储密钥信息的情况下恢复加密会话。 当打开第一个连接并生成密钥时,服务器会加密连接状态并将其发送给客户端(以会话票证的形式)。 因此,当连接恢复时,客户端将包含会话密钥的会话票证发送回服务器。 票证本身使用临时密钥(会话票证密钥)加密,该密钥存储在服务器上,必须分发给集群解决方案中处理 SSL 的所有前端服务器。[10]。 因此,如果临时服务器密钥被泄露,例如,当它们被长期存储时(OpenSSL、nginx、Apache 默认情况下在程序运行的整个时间存储它们;流行站点),会话票证的引入可能违反 PFS使用密钥几个小时,最多几天)。

这里RTT不为零,至少需要交换ClientHello和ServerHello,然后加上Finished,客户端就已经可以发送数据了。 但在这里应该记住,我们没有 Web,它有一堆新打开的连接,而是一个信使,它的连接通常是一个或多或少长期存在的、相对较短的网页请求——一切都是里面多路复用。 也就是说,如果我们没有遇到非常糟糕的地铁部分,这是完全可以接受的。

忘了别的东西? 写在评论里。

待续!

在本系列文章的第二部分,我们将考虑组织问题而不是技术问题——方法、意识形态、界面、对用户的态度等。 但是,基于此处提供的技术信息。

第三部分将继续分析技术组件/开发经验。 您将特别学到:

  • 各种 TL 类型的混乱的延续
  • 关于通道和超组的未知事物
  • 比对话比花名册更糟糕
  • 关于绝对与相对消息寻址
  • photo 和 image 有什么区别
  • 表情符号如何干扰斜体文本

和其他拐杖! 敬请关注!

来源: habr.com

添加评论