MTProto 移动协议的二进制数据序列化

MTProto 操作要求基本数据类型、复合数据类型以及以这些数据类型作为参数传递或返回的查询,均以二进制格式(即序列化)传输。TL语言用于描述要序列化的数据类型。

一般定义

就我们的目的而言,我们可以将类型与其(序列化的)值集合联系起来,这些值被理解为 32 位数字的字符串(有限序列)(以小端序传输)。

所以:

组合子、构造函数、复合数据类型

盒装和裸装字体

从概念上讲,所有地方都应该只使用装箱类型。然而,为了提高速度和节省空间,必须使用裸类型(例如,一个包含 10,000 个裸 int 值的数组大小为 40,000 字节,而装箱的 int 值占用的空间是其两倍;因此,在传输大型整数标识符数组时,使用裸Vector int类型比使用装箱类型更高效Vector Int)。此外,所有基本类型(int、long、double、string)都是裸类型。

如果一个装箱类型是元数为 r 的多态类型,那么它的任何派生裸类型也同样如此。换句话说,如果定义了intCouple {alpha:Type} int alpha = IntCouple alpha`intCouple`,那么之后,作为标识符的 `intCouple` 在组合子描述(以及构造函数和类型描述)中也将是元数为 1 的多态类型。`intCouple`、`intCouple` 和 `intCouple` 这三种表示法intCouple X是%(IntCouple X)等价%IntCouple X的。

基本类型

基本类型既有裸类型(int、long、double、string),也有装箱类型(Int、Long、Double、String)。它们的构造函数标识符与相应的裸类型名称一致。它们的伪描述如下所示:

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

因此,int构造函数索引号,例如,是字符串的 CRC32 值"int ? = Int"。

裸类型的值int恰好是所有单元素序列,即介于 -2^31 和 2^31-1 之间的数字在此情况下表示它们本身。类型的值long是包含 64 位有符号数(同样是小端序)的双元素序列。类型为的值double同样是包含 64 位实数(标准双精度格式)的双元素序列。最后,类型的值string会根据被序列化的字符串长度 L 而有所不同:

对象伪类型

伪类型Object是一种可以取值于模式中任何已装箱类型的“类型”。这有助于快速定义诸如随机项列表之类的类型,而无需使用多态类型。最好不要滥用此功能,因为它会导致使用动态类型。尽管如此,如果没有 Object 伪类型,我们很难想象 PHP 和 JSON 中常见的那些数据结构会是什么样子。

建议TypedObject尽可能改用:

object X:Type value:X = TypedObject;

内置复合类型:向量和关联数组

Vector t 多态伪类型是一个“类型”,其值为任意类型 t 的值序列,可以是装箱的,也可以是裸的。

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

序列化始终使用同一个构造函数“vector”(const 0x1cb5c415 = crc32("vector t:Type # [ t ] = Vector t")),该构造函数不依赖于类型为 t 的变量的具体值。Vector t 类型的值由相关构造函数编号的索引号后跟 N(向量中的元素个数)以及 N 个类型为 t 的值组成。可选参数 t 的值不参与序列化,因为它由结果类型派生而来(在反序列化之前始终已知)。

多态伪类型 `IntHash t` 和 `StrHash t` 是关联数组,分别将整数和字符串键映射到类型为 `t` 的值。实际上,它们是包含裸二元组 (int, t) 或 (string, t) 的向量:

coupleInt {t:Type} int t = CoupleInt t; intHash {t:Type} (vector %(CoupleInt t)) = IntHash t; coupleStr {t:Type} string t = CoupleStr t; strHash {t:Type} (vector %(CoupleStr t)) = StrHash t; 

在这种情况下,百分号表示取与括号中的装箱类型对应的裸类型;无论参数的值如何,所讨论的装箱类型都只能有一个构造函数。

键可以排序,也可以按其他顺序排列(例如 PHP 数组)。对于键已排序的关联数组,可以使用别名 IntSortedHash 或 StrSortedHash:

intSortedHash {t:Type} (intHash t) = IntSortedHash t; strSortedHash {t:Type} (strHash t) = StrSortedHash t; 

多态类型构造器

多态类型的构造函数不依赖于该多态类型所应用的具体类型。在计算构造函数时,可选参数(通常包含类型变量并置于花括号内)不再是可选的(花括号被移除),此外,所有括号也被移除。因此,

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

对应于构造函数编号 crc32("vector t:Type # [ t ] = Vector t") = 0x1cb5c415。在(反)序列化期间,可选变量 t 的具体值是从始终已知的结果类型(即正在序列化或反序列化的对象)派生出来的,并且永远不会显式地序列化。

以前,必须事先知道每种多态类型将应用于哪些特定的变量类型。为了实现这一点,类型系统使用了如下形式的字符串:

polymorphic_type_name type_1 ... type_N;

例如,

Vector int; Vector string; Vector Object; 

现在他们被忽视了。

另见TL 中的多态性。

在这种情况下,Object 伪类型允许使用 Vector Object 来存储任何类型的列表(任何装箱类型的值)。由于裸类型在数据量较小时效率很高,因此在实践中不太可能需要比上述情况更复杂的场景。

字段名称

假设我们需要将用户表示为包含一个整数(用户 ID)和两个字符串(名字和姓氏)的三元组。所需的数据结构是整数、字符串、字符串三元组,可以声明如下:

user int string string = User;

另一方面,一个组也可以用类似的三元组来描述,该三元组包括组 ID、组名称和描述:

group int string string = Group;

为了明确区分用户和组,可以方便地给部分或全部字段分配名称:

user id:int first_name:string last_name:string = User; group id:int title:string description:string = Group; 

如果以后需要通过添加一些额外的字段来扩展用户类型,可以按如下方式实现:

userv2 id:int unread_messages:int first_name:string last_name:string in_groups:vector int = User;

除此之外,这种方法还有助于定义属于同一类型的不同构造函数的字段之间的正确映射,在它们之间进行转换,以及将类型值转换为具有字符串键的关联数组(如果定义了字段名称,则字段名称是此类键的自然选择)。

目标语言

参见TL 语言