MTProto 移动协议的二进制数据序列化
MTProto 操作要求基本数据类型、复合数据类型以及以这些数据类型作为参数传递或返回的查询,均以二进制格式(即序列化)传输。TL语言用于描述要序列化的数据类型。
一般定义
就我们的目的而言,我们可以将类型与其(序列化的)值集合联系起来,这些值被理解为 32 位数字的字符串(有限序列)(以小端序传输)。
所以:
- 在这种情况下,字母(A) 是一组 32 位数字(通常是有符号的,即介于 -2^31 和 2^31 - 1 之间)。
- 在这种情况下,值与字母表 A 中的一个字符串相同,即一个有限的(可能为空的)32 位数字序列。所有此类序列的集合被指定为A*。
- 就我们的目的而言,类型等同于该类型的合法值集合,即集合 T,它是 A* 的子集,并且是一个前缀码(即 T 中的任何元素都不能是其他元素的前缀)。因此,A* 中的任何序列最多只能包含一个属于 T 的前缀。
- T 类型的值是任何序列(值),它是 A* 的子集 T 的成员。
- 兼容类型是指 T 和 T' 作为 A* 的子集不相交的类型,使得 T 和 T' 的并集是一个前缀码。
- 协调类型系统是一个有限或无限的类型集合 T_1, ..., T_n, ...,其中该集合中的任意两个类型都是兼容的。
- 数据类型与上述定义中的类型含义相同。
- 函数类型是一种描述函数的类型;它并非上述定义意义上的类型。最初,我们忽略函数类型的存在,仅描述数据类型;然而,实际上,函数类型稍后将在该系统的某些扩展中使用所谓的临时组合子来实现。
组合子、构造函数、复合数据类型
-
组合子是一种函数,它接受特定类型的参数,并返回另一种类型的值。我们通常关注参数和结果类型均为数据类型(而非函数类型)的组合子。
-
组合子的元数是一个非负整数,表示组合子参数的数量。
-
组合子标识符是以小写罗马字母开头的标识符,用于唯一标识一个组合子。
-
组合器编号或组合器名称是一个 32 位数字(即 A 的一个元素),用于唯一标识一个组合器。通常情况下,它是包含组合器描述的字符串的 CRC32 校验值,去掉末尾的分号,并在相邻词素之间留一个空格。该值始终在 0x01000000 到 0xffffff00 的范围内。最高的 256 个值保留给用于传输函数的所谓时序逻辑组合器。我们通常用单引号将组合器名称表示为“ combinator ”。
-
组合器描述是一个字符串,格式为,combinator_name type_arg_1 ... type_arg_N = type_res;其中N表示组合器的元数,type_arg_i是第 i 个参数的类型(或者更确切地说,是包含组合器名称的字符串),type_res是组合器值的类型。
-
构造函数是一个不可计算(归约)的组合子。它用于表示复合数据类型。例如,带有描述的组合子“int_tree”以及int_tree IntTree int IntTree = IntTree组合子本身empty_tree = IntTree,可以用来定义一个名为“IntTree”的复合数据类型,该类型以二叉树的形式取值,其中二叉树的节点为整数。
-
函数(函数式组合器)是一种组合器,其计算(约简)的条件是提供所需数量且类型正确的参数。计算结果是一个仅由构造函数和基本类型值构成的表达式。
-
范式是仅由构造函数和基本类型值组成的表达式;通常是计算函数的结果。
-
类型标识符通常以罗马字母大写字母开头,用于唯一标识类型。
-
类型编号或类型名称是一个 32 位数字,用于唯一标识一个类型;它通常是类型构造函数的描述的 CRC32 值的总和。
-
(复合)类型 T 的描述是所有接受类型T值的构造函数的描述集合。通常以文本形式编写,每个字符串包含一个构造函数的描述。例如,以下是类型“IntTree”的描述:
int_tree IntTree int IntTree = IntTree; empty_tree = IntTree;
-
多态类型是指其描述中包含参数(类型变量)而非实际类型的类型;它大致类似于 C++ 中的模板。以下是对 `Type` 的描述,List alpha其中`Type`List是一个参数数为 1 的多态类型(即依赖于单个参数),而 `a`alpha是一个类型变量,它作为构造函数的可选参数(用花括号括起来)出现:
cons {alpha:Type} alpha (List alpha) = List alpha; nil {alpha:Type} = List alpha;
-
(复合)类型 T 的值是 A* 算法中任意一个序列,其格式为,其中 constr_num 是某个构造函数Cconstr_num arg1 ... argN的索引号,该构造函数接受类型T的值,arg_i 是类型T_i的值,它是构造函数C 的第 i 个参数的类型。例如,假设组合子 int_tree 的索引号为 17,而组合子 empty_tree 的索引号为 239。那么,类型 T 的值例如为 ,更方便的写法是。从高级语言的角度来看,这表示。IntTree17 17 239 1 239 2 239'int_tree' 'int_tree' 'empty_tree' 1 'empty_tree' 2 'empty_tree'int_tree (int_tree (empty_tree) 1 (empty_tree)) 2 (empty_tree): IntTree
-
模式是所有(复合)数据类型描述的集合。它用于定义某种约定俗成的类型系统。
盒装和裸装字体
- 装箱类型是指其所有值均以构造函数编号开头的类型。由于每个构造函数都有一个唯一确定的值类型,因此任何装箱类型值的第一个数字都唯一地定义了其类型。这保证了各种装箱类型共同构成一个协调一致的类型系统。装箱类型标识符始终大写。
- 裸类型是指其值不包含构造函数编号的类型,构造函数编号是隐含的。裸类型标识符始终与隐含构造函数的名称一致(因此以小写字母开头),并且可能在前面添加百分号 (%)。此外,如果 `A`X是一个装箱类型,且只有一个构造函数,则 `A`%X指向对应的裸类型。裸类型的值与从对应的装箱类型(即所选构造函数的结果类型)的值集中去掉第一个数字(即外部构造函数索引号)后得到的一系列数字相同,这些数字从所选构造函数的索引号开始。例如,`A`是使用 `A` 定义的裸类型3 4的值。对应的装箱类型是`A`;如果 `A` 的构造函数索引号为 404 ,则`A` 是与裸类型的值对应的装箱类型的值(也称为 ` A` 和 `B` ;后一种形式在概念上更可取,但更长)。int_coupleint_couple int int = IntCoupleIntCoupleint_couple404 3 4IntCoupleint_couple%int_couple%IntCouple
从概念上讲,所有地方都应该只使用装箱类型。然而,为了提高速度和节省空间,必须使用裸类型(例如,一个包含 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 而有所不同:
- 如果 L <= 253,则序列化包含一个值为 L 的字节,然后是 L 个字节的字符串,后跟 0 到 3 个包含 0 的字符,使得该值的总长度能被 4 整除,然后所有这些都被解释为 int(L/4)+1 个 32 位数字的序列。
- 如果 L >= 254,则序列化包含字节 254,后跟 3 个长度为 L 的字符串字节,后跟 L 个字节的字符串,再后跟 0 到 3 个空填充字节。
对象伪类型
伪类型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 语言