erlAarango 二进制序列化库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

23 KiB

速度包 (VPack) Version 1 VelocyPack (VPack) 是一种快速紧凑的序列化格式

概论 VPack 是面向(无符号)字节的,因此 VPack 值只是字节序列并且与平台无关。值不一定对齐,因此必须正确组织对较大子值的所有访问,以避免 CPU 的对齐假设。

值类型 我们描述了一个单一的 VPack 值,它本质上是递归的,但驻留在一个连续的内存块中(有两个例外,见下文)。假设值从地址 A 开始,第一个字节 V 指示手头 VPack 值的类型(通常是长度):

我们首先给出一个概述,并提供一个简短但准确的描述以供参考,对于数组和对象,请参见下面的详细信息:

0x00 : none - 这表示没有任何类型和值,这在 VPack 值中是不允许的 0x01 : 空数组 0x02 : 无索引表的数组(所有子项字节长度相同),1-byte字节长度 0x03 : 无索引表的数组(所有子项字节长度相同),2-byte字节长度 0x04 :无索引表的数组(所有子项字节长度相同),4字节字节长度 0x05 : 无索引表的数组(所有子项字节长度相同),8字节字节长度 0x06 : 具有 1 字节索引表偏移量、bytelen 和 # subvals 的数组 0x07 : 具有 2 字节索引表偏移量、bytelen 和 # subvals 的数组 0x08 : 具有 4 字节索引表偏移量、bytelen 和 # subvals 的数组 0x09 : 具有 8 字节索引表偏移量、bytelen 和 # subvals 的数组 0x0a : 空对象 0x0b :具有 1 字节索引表偏移量的对象,按属性名称、1 字节 bytelen 和 # subvals 排序 0x0c :具有 2 字节索引表偏移量的对象,按属性名称、2 字节 bytelen 和 # subvals 排序 0x0d : 具有 4 字节索引表偏移量的对象,按属性名称、4 字节 bytelen 和 # subvals 排序 0x0e : 具有 8 字节索引表偏移量的对象,按属性名称、8 字节 bytelen 和 # subvals 排序 0x0f :具有 1 字节索引表偏移量的对象,未按属性名称、1 字节 bytelen 和 # subvals 排序 -废弃的 0x10 :具有 2 字节索引表偏移量的对象,未按属性名称、2 字节 bytelen 和 #subvals 排序 - 废弃的 0x11 :具有 4 字节索引表偏移量的对象,未按属性名称、4 字节 bytelen 和 #subvals 排序 - 废弃的 0x12 :具有 8 字节索引表偏移量的对象,未按属性名称、8 字节 bytelen 和 #subvals 排序 - 废弃的 0x13 : 紧凑型数组,无索引表 0x14 : 紧凑型对象,无索引表 0x15- 0x16:保留 0x17 : illegal - 此类型可用于指示嵌入应用程序中非法的值 0x18 : null 0x19 : false 0x1a : true 0x1b : double IEEE-754, 8 bytes followed, stored as little endian uint64 equivalent 0x1c :自纪元以来以毫秒为单位的 UTC 日期,存储为 8 字节有符号整数,小端,二进制补码 0x1d : external (only in memory): 一个 char* 指向内存中的实际位置,另一个 VPack 项所在的位置,不允许出现在磁盘或网络上的 VPack 值中 0x1e : minKey,将 < 与所有其他值进行比较的无意义值 0x1f : maxKey,比较 > 比所有其他值的无意义值 0x20- 0x27: signed int, little endian, 1 to 8 bytes, number is V - 0x1f, twos complement 0x28- 0x2f: uint, little endian, 1 to 8 bytes, number is V -0x27 0x30- 0x39: 小整数 0, 1, ... 9 0x3a- 0x3f: 小的负整数 -6, -5, ..., -1 0x40- 0xbe: UTF-8-string,使用V- 0x40bytes(不是Unicode字符!),长度可以0x40是0,空字符串也可以,最大长度是126,注意这里的字符串不是以零结尾的,可能包含NUL字节 0xbf : long UTF-8-string,接下来的 8 个字节是字符串的长度,以字节为单位(不是 Unicode 字符)作为小端无符号整数,注意长字符串不是零终止的,可能包含 NUL 字节 0xc0- 0xc7:二进制 blob,接下来的 V -0xbf字节是 blob 的字节长度,注意二进制 blob 不是零终止的 0xc8- 0xcf:正长压缩 BCD 编码浮点数,V -0xc7字节跟随以小端方式编码尾数的长度(以字节为单位)。紧随其后的是编码(10 次方)指数的 4 个字节,尾数将乘以该指数,存储为小端二进制补码带符号的 32 位整数。之后,按照开头的长度信息指定的字节数,每个字节用big-endian packed BCD编码两个数字。示例:12345 十进制可以编码为 c8 03 00 00 00 00 01 23 45或 c8 03 ff ff ff ff 12 34 50 0xd0- 0xd7:负长压缩 BCD 编码浮点数,V -0xcf字节跟随以小端方式编码尾数的长度(以字节为单位)。之后,与上面的正长打包 BCD 编码浮点数相同。 0xd8- 0xed:保留 0xee- 0xef:逻辑类型的值标记 0xf0- 0xff:自定义类型 数组 空数组只是一个字节0x01。

接下来我们将描述类型案例0x02,0x09请参阅下面的特殊紧凑类型0x13。

非空数组类似于以下之一:

one of 0x02 to 0x05 BYTELENGTH OPTIONAL UNUSED: padding sub VPack values 或者

0x06 BYTELENGTH in 1 byte NRITEMS in 1 byte OPTIONAL UNUSED: 6 bytes of padding sub VPack values INDEXTABLE with 1 byte per entry 或者

0x07 BYTELENGTH in 2 bytes NRITEMS in 2 bytes OPTIONAL UNUSED: 4 bytes of padding sub VPack values INDEXTABLE with 4 byte per entry 或者

0x08 BYTELENGTH in 4 bytes NRITEMS in 4 bytes sub VPack values INDEXTABLE with 4 byte per entry 或者

0x09 BYTELENGTH in 8 bytes sub VPack values INDEXTABLE with 8 byte per entry NRITEMS in 8 bytes 如果一个类型允许任何可选填充,则填充必须包含恰好等于填充长度、BYTELENGTH 长度和 NRITEMS(如果存在)长度总和为 8 的字节数。如果 BYTELENGTH 的长度是已经 8,不允许填充。整个填充必须包含零字节 (ASCII NUL)。

数字(用于字节长度、INDEXTABLE 中的子值和偏移量的数量)是小端无符号整数,类型和使用 1 个字节,类型0x02和使用0x062 个字节,类型0x03和使用0x074 个字节,类型 和0x04使用0x088 个字节。0x050x09

NRITEMS 是如上所述的单个数字。

INDEXTABLE 包括:

对于类型0x06-0x09一个偏移量数组(未对齐,采用上述数字格式)较早的偏移量位于较低的地址。偏移量是从 VPack 值的开始测量的。 0x06类型的非空数组0x09有一个小头,包括它们的字节长度、子值的数量,然后是所有子值,最后是包含子值偏移量的索引表。要找到索引表,请找到子值的数量,然后是末尾,并从中找到索引表的基数,并考虑其条目的宽度。

对于 to 的类型0x02,0x05没有偏移表,也没有项目数。第一项从地址 A+2、A+3、A+5 或 A+9 开始,具体取决于字节长度字段的类型和宽度。请注意以下特殊规则:在填充零字节的一些运行之后,允许第一个子值的实际位置更靠后。

例如,如果两个字节长度 (BYTELENGTH) 都使用 2 个字节,则随后允许有 4 个零字节的可选填充,并且实际的 VPack 子值可以从 A+9 开始。这是为了让构建 VPack 值的程序有机会在开始时保留 8 个字节,然后才发现更少的字节足以写入字节长度。可以通过找到第一个子值及其字节长度并将可用空间量除以它来确定子值的数量。

对于偏移表的类型0x06,0x09描述了子值所在的位置。子值不必在子值数字段之后立即开始。

如上所述,允许包含可选的填充。在这里,任何填充都必须由一系列连续的零字节 (ASCII NUL) 组成,并且长度必须能够将 BYTELENGTH 的长度和 NRITEMS 的长度填满为 8。

例如,如果 BYTELENGTH 和 NRITEMS 都可以用 2 个字节表示,则它们的长度之和为 4。因此可以在此处添加 4 个字节的填充,以便第一个子值可以位于地址 A+9。

8 字节数字的情况(type 0x05)有一个例外:在这种情况下,元素的数量被移到索引表后面。这是为了在一开始保留 8 个字节并且后来注意到字节长度需要所有 8 个字节时不移动内存。对于这种情况,不允许包含任何填充。

所有偏移量均从基准 A 开始测量。

示例:

[1,2,3]有十六进制转储

02 05 31 32 33 在最紧凑的表示中,但以下同样可能,但不一定建议使用:

例子:

03 06 00 31 32 33

04 08 00 00 00 31 32 33

05 0c 00 00 00 00 00 00 00 31 32 33

06 09 03 31 32 33 03 04 05

07 0e 00 03 00 31 32 33 05 00 06 00 07 00

08 18 00 00 00 03 00 00 00 31 32 33 09 00 00 00 0a 00 00 00 0b 00 00 00

09 2c 00 00 00 00 00 00 00 31 32 33 09 00 00 00 00 00 00 00 0a 00 00 00 00 00 00 00 0b 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 请注意,不建议以太长的格式对短数组进行编码。

我们现在描述特殊类型0x13,它对于特别紧凑的数组表示很有用。请注意,这在某种程度上违反了 VelocyPack 格式的原则,因为不再可能快速访问子值,因此必须扫描数组中的所有项目以找到特定的项目。但是,VelocyPack 的某些用例只需要顺序访问(例如 JSON 转储)并且对紧凑性有特殊需求。

这种数组类型的整体格式是

0x13 as type byte BYTELENGTH sub VPack values NRITEMS 根本没有索引表,尽管子 VelocyPack 值可以有不同的字节大小。BYTELENGTH 和 NRITEMS 以我们现在描述的特殊格式编码。

BYTELENGTH 由 1 到 8 个字节组成,除最后一个字节外,所有字节都设置了高位。因此,高位决定实际使用了多少字节。所有这些位的低 7 位以小端方式共同构成实际字节长度。也就是说,地址 A+1 的字节包含字节长度的最低有效 7 位(0 到 6),地址 A+2 的后续字节包含位 7 到 13,依此类推。由于字节总数限制为 8,因此它对最多 56 位的无符号整数进行编码,这是这种紧凑数组表示的大小的总体限制。

NRITEMS 条目的编码基本相同,只是它在内存中的排列顺序相反。也就是说,必须使用 BYTELENGTH 找到数组值的末尾并返回字节,直到找到高位重置的字节。最后一个字节(在最高内存地址处)包含 NRITEMS 值的最低有效 7 位,第二个字节包含 7 到 13 位,依此类推。

下面是一个例子,数组 [1, 16] 可以编码如下:

13 06 31 28 10 02 对象 空对象只是一个字节0x0a。

接下来我们将描述类型案例0x0b,0x12请参阅下面的特殊紧凑类型0x14。

非空对象如下所示:

one of 0x0b - 0x12 BYTELENGTH optional NRITEMS sub VPack values as pairs of attribute and value optional INDEXTABLE NRITEMS for the 8-byte case 数字(用于字节长度、INDEXTABLE 中的子值和偏移量的数量)是小端无符号整数,类型和使用 1 个字节,类型0x0b和使用0x0f2 个字节,类型0x0c和使用0x104 个字节,类型 和0x0d使用0x118 个字节。0x0e0x12

NRITEMS 是如上所述的单个数字。

INDEXTABLE 包括:

偏移量数组(未对齐,采用上述数字格式)较早的偏移量位于较低的地址。偏移量是从 VPack 值的开头开始测量的。 非空对象有一个小头,包括它们的字节长度、子值的数量,然后是所有子值,最后是包含子值偏移量的索引表。要查找索引表,请查找子值的数量,然后是结尾,然后根据索引表的基数,考虑其条目的宽度。

对于所有类型,偏移表描述了子值所在的位置。子值不必在子值数字段之后立即开始。出于性能原因,在构建值时,可能需要为字节长度和子值的数量保留 8 个字节而不是填充间隙,即使后来证明偏移量和字节长度仅使用 2 个字节,比如.

有一种特殊情况:空对象被简单地存储为单个字节0x0a。

还有一个例外:对于 8 字节数字 ( 0x12),子值的数量存储在 INDEXTABLE 后面。这是为了在一开始保留 8 个字节并且后来注意到字节长度需要所有 8 个字节时不移动内存。

所有偏移量均从基准 A 开始测量。

每个条目由两部分组成,键和值,它们被编码为如上所述的普通 VPack 值,第一个总是以字节开头的短或长 UTF-8 字符串0x40-0xbf如下所述。第二个是任何其他 VPack 值。

有一个扩展:对于密钥,可以使用正小整数值0x30-或以类型字节-0x39开头的无符号整数。任何此类整数值都是外部给定属性名称表的索引。当只有很少的属性名称出现或某些属性名称经常重复时,这些很方便。编码此类属性名称表的标准方法是作为此处指定的 VPack 字符串数组。0x280x2f

对象总是与排序的键/值对一起存储,通过每个嵌套级别上的键的字节比较来排序。排序有一些开销,但将允许稍后在对数时间内查找键。注意,只需要对索引表进行排序,并不要求这些表中的偏移量都是递增的。由于索引表位于实际子值之后,因此可以通过线性写入来构建复杂的 VPack 值。

示例:对象{"a": 12, "b": true, "c": "xyz"}可以具有 hexdump:

0b 13 03 41 62 1a 41 61 28 0c 41 63 43 78 79 7a 06 03 0a 相同的对象可以用具有更长条目的索引表完成,如本例所示:

0d 22 00 00 00 03 00 00 00 41 62 1a 41 61 28 0c 41 63 43 78 79 7a 0c 00 00 00 09 00 00 00 10 00 00 00 与类型0x0c和 2 字节偏移量、字节长度和子值数量或类型0x0e和 8 字节数字类似。

请注意,不建议使用太长的索引表对短对象进行编码。

特殊紧凑对象 我们现在描述特殊类型0x14,它对于特别紧凑的对象表示很有用。请注意,这在某种程度上违反了 VelocyPack 格式的原则,因为不再可能快速访问子值,因此必须扫描对象中的所有键/值对以找到特定的键/值对。但是,VelocyPack 的某些用例只需要顺序访问(例如 JSON 转储)并且对紧凑性有特殊需求。

这个对象类型的整体格式是

0x14 as type byte BYTELENGTH sub VPack key/value pairs NRPAIRS 根本没有索引表,尽管子 VelocyPack 值可以有不同的字节大小。BYTELENGTH 和 NRPAIRS 以我们现在描述的特殊格式编码。它与特殊的紧凑数组类型相同0x13,为了完整起见,我们在此重复。

BYTELENGTH 由 1 到 8 个字节组成,除最后一个字节外,所有字节都设置了高位。因此,高位决定实际使用了多少字节。所有这些位的低 7 位以小端方式共同构成实际字节长度。也就是说,地址 A+1 的字节包含字节长度的最低有效 7 位(0 到 6),地址 A+2 的后续字节包含位 7 到 13,依此类推。由于字节总数限制为 8,因此它对最多 56 位的无符号整数进行编码,这是这种紧凑数组表示的大小的总体限制。

NRPAIRS 条目的编码基本相同,只是它在内存中的排列顺序相反。也就是说,必须使用 BYTELENGTH 找到数组值的末尾并返回字节,直到找到高位重置的字节。最后一个字节(在最高内存地址处)包含 NRPAIRS 值的最低有效 7 位,第二个字节包含 7 到 13 位,依此类推。

这是一个例子,对象{"a":1, "b":16}可以编码如下:

14 0a 41 61 31 42 62 28 10 02 双打 类型0x1b指示使用类型字节后的 8 个字节的双精度 IEEE-754 值。为了保证平台独立性,字节顺序的细节如下。编码是通过使用 memcpy 将内部双精度值复制到 uint64_t 来完成的。这个 64 位无符号整数然后作为小端 8 字节整数存储在 VPack 值中。解码工作在相反的方向。这应该在实践中解决 IEEE-754 中未确定的字节顺序。

日期 类型0x1c表示一个带符号的 64 整数,存储在紧跟在类型之后的 8 字节小端二进制补码符号中。该值表示自纪元以来以毫秒为单位测量的通用 UTC 时间,即 1970 年 1 月 1 日世界标准时间 00:00。

外部 VPack 值 此类型仅用于内存中,不适用于磁盘或网络上的数据交换。因此,我们只需要指定后面的k字节是一个char在当前架构上的memcpy即可。该 char 指向内存中其他地方的实际 VPack 值。

人工最小和最大键 0x1etypes和的这些值0x1f除了分别比较小于或大于任何其他 VPack 值之外没有任何意义。这个想法是,这些可以用于定义所有 VPack 值的总顺序的系统,以指定无限间隔的左端或右端。

整数类型 有不同的方法来指定整数。对于 -6 到 9(含)的小值,范围内有特定类型的字节0x30以0x3f允许存储在单个字节中。之后是有符号和无符号整数类型,它们可以在类型字节中编码使用的字节数(范围0x20-0x27有符号和0x28-0x2f无符号)。

空值和布尔值 这三个值使用单个字节来存储相应的 JSON 值。

字符串 字符串存储为 UTF-8 编码的字节序列。有两种变体,一种是短的,一种是长的。在短的一个中,字节长度(不是 UTF-8 字符的数量)直接在类型中编码,这可以达到并包括字节长度 126。用于此的类型0x40和0xbe 字节长度是 V - 0x3f,如果 V 是类型字节。对于长度超过 126 字节的字符串,类型字节是0xbf,字符串的字节长度存储在类型字节之后的前 8 个字节中,使用小端无符号整数表示。实际字符串跟在这 8 个字节之后。在这两种情况下都没有终止零字节,并且字符串可能包含零字节。

二进制数据 0xc0允许0xc7将任意二进制字节序列存储为 VPack 值的字节类型。格式如下: 如果V是type byte,那么V- 0xbfbytes跟在它后面做一个little endian无符号整数表示二进制数据的长度,直接跟在这些length字节之后。不保证对齐。内容完全取决于用户。

压缩 BCD 长浮点数 这些类型用于表示任意精度的十进制数。正数和负数有不同的类型。这些值的整体格式是:

one of 0xc8 - 0xcf (positive) or of 0xd0 - 0xd7 (negative) LENGTH OF MANTISSA in bytes EXPONENT (as 4-byte little endian signed two's complement integer) MANTISSA (as packed BCD-encoded integer, big-endian) 类型字节描述了数字的符号以及用于指定尾数字节长度的字节数。通常,如果 V 是类型字节,则 V - 0xc7(在正例中)或 V - 0xcf(在负例中)字节用于尾数的长度,直接在字节长度之后存储为小端无符号整数。在此之后紧跟 4 个字节(小端符号二进制补码整数)来指定指数。指数之后是实际的尾数字节。

使用压缩 BCD,以便每个字节存储恰好 2 个十进制数字,如0x34十进制数字 34。因此,尾数始终具有偶数个十进制数字。请注意,尾数以大端形式存储,以提高解析和转储的效率。这导致了“邪恶的半字节问题”:当 JSON 解析器看到一个较长数字的开头时,它不知道后面是偶数还是奇数。但是,出于效率原因,它希望在读取输入时开始将字节写入输出。这就是指数派上用场的地方,下面的例子说明了这一点。12345 十进制可以编码为:

c8 03 00 00 00 00 01 23 45 c8 03 ff ff ff ff 12 34 50 前一种编码在第一个字节中放置一个前导 0 并使用指数 0,后者编码直接开始将两个十进制数字放入一个字节然后最后必须使用指数 -1 来“擦除”尾随 0,由4字节序列ff ff ff ff。

因此,邪恶的半字节问题得到解决,解析(实际上是转储)可以变得高效。

标记 类型0xee-0xef用于标记值以实现逻辑类型。

例如,如果类型0x1c不存在,数据库驱动程序可以将时间戳对象(JavaScript 中的 Date、Java 中的 Instant 等)序列化为 Unix 时间戳,一个 64 位整数。假设缺少模式,在反序列化时就不可能从时间戳中分辨出整数并相应地反序列化该值。

类型标记通过将整数标记附加到值来解决这个问题,然后在反序列化值时可以读取该标记,例如,tag=1 是时间戳,应该使用相关的时间戳类。

标记值是单独指定的,应用程序也可以指定它们自己的值,以使数据库驱动程序将它们的特定数据类型反序列化为适当的类(包括模型)。

本质上这是文档部分的对象关系映射。

类型的格式是:

0xee TAG number in 1 byte sub VPack value 或者

0xef TAG number in 8 bytes, little-endian encoding sub VPack value 自定义类型 请注意,自定义类型通常不应用于数据交换,而只能在系统内部使用。尽管如此,规范这一部分的设计使得可以通过通用方法导出每个自定义数据类型的字节长度。

存在以下用户定义类型:

0xf0 : 1 字节有效载荷,紧跟在类型字节之后 0xf1 : 2字节有效载荷,紧跟在类型字节之后 0xf2 :4字节有效载荷,紧跟在类型字节之后 0xf3 : 8 字节有效载荷,紧跟在类型字节之后 0xf4- 0xf6: 有效载荷的长度由紧跟在类型字节之后的单个无符号字节描述,那么多字节的有效载荷紧随其后 0xf7- 0xf9:有效载荷的长度由紧跟在类型字节之后的两个字节(小端无符号整数)描述,那么多字节的有效载荷如下 0xfa- 0xfc:有效载荷的长度由紧跟在类型字节之后的四个字节(小端无符号整数)描述,那么多字节的有效载荷如下 0xfd- 0xff:有效载荷的长度由紧跟在类型字节之后的八个字节(小端无符号整数)描述,那么多字节的有效载荷如下 注:类型0xf4中0xff的“payload”是指不包括长度规格的实际数据。

可移植性 序列化的布尔值、整数、字符串、数组、对象等都有定义的字节顺序和长度,这是平台无关的。这些类型在序列化的 VelocyPack 中是完全可移植的。

在可移植性方面仍有一些注意事项:

可以在 64 位系统上构建非常大的值,但可能无法在 32 位系统上读回它们。这是因为与 64 位系统相比,32 位系统上的最大内存分配大小可能受到严格限制,即 32 位操作系统可能根本不允许分配大于 4 GB 的缓冲区。这不是 VelocyPack 的限制,而是 32 位架构的限制。如果所有 VelocyPack 值都保持足够小,以便它们远低于 32 位长度边界,那么这应该无关紧要。

VelocyPack 类型External只包含一个指向内存的原始指针,它应该只在内存中建立 VelocyPack 值期间使用。External类型不应该用于 VelocyPack 值,这些值被序列化并持久存储,然后从持久性中读回。无论如何都这样做是不可移植的,并且还会带来安全风险。不对任何序列化的数据使用外部类型将完全避免此问题。

VelocyPack 类型Custom完全是用户自定义的,它们没有默认实现。因此,如果考虑到这些自定义类型绑定的可移植性,则由嵌入器来实现可移植性。

VelocyPack Double值以特定方式序列化为整数等价物,然后反序列化回覆盖内存中 IEEE-754 双精度浮点值的整数。我们发现这足以满足我们的需求,尽管至少在理论上某些系统可能存在可移植性问题。

以下内容用作我们“在现实世界中合理便携”假设的支持:

因此,广泛使用的 IEEE 754 浮点标准没有指定字节序可能显得很奇怪。 [17] 从理论上讲,这意味着即使是一台机器写入的标准 IEEE 浮点数据也可能无法被另一台机器读取。然而,在现代标准计算机(即,实现 IEEE 754)上,实际上可以安全地假设浮点数的字节序与整数的字节顺序相同,从而使转换直接进行,而不管数据类型如何。