查看源代码 外部项格式

简介

外部项格式主要用于 Erlang 的分布机制中。

由于 Erlang 具有固定数量的类型,因此程序员无需为某些应用程序中使用的外部格式定义规范。所有 Erlang 项都有外部表示,不同项的解释是特定于应用程序的。

在 Erlang 中,BIF erlang:term_to_binary/1,2 用于将项转换为外部格式。要将二进制数据编码转换为项,则使用 BIF erlang:binary_to_term/1

当跨节点边界发送消息时,分布会隐式地执行此操作。

项格式的总体格式如下

11N
131标签数据

注意

当消息在连接的节点之间传递并且使用了分布头时,包含版本号(131)的第一个字节将从分布头之后的项中省略。这是因为版本号是由分布头中的版本号隐含的。

压缩项格式如下

114N
13180未压缩大小Zlib 压缩数据

未压缩大小(大端字节序的无符号 32 位整数)是数据在压缩之前的尺寸。压缩数据在展开后具有以下格式

1未压缩大小
标签数据

编码原子

从 ERTS 9.0 (OTP 20) 开始,原子可能包含任何 Unicode 字符。

通过节点分布发送的原子始终使用 ATOM_UTF8_EXTSMALL_ATOM_UTF8_EXTATOM_CACHE_REF 以 UTF-8 编码。

使用 erlang:term_to_binary/1,2erlang:term_to_iovec/1,2 编码的原子默认仍对仅包含 Latin-1 字符(Unicode 代码点 0-255)的原子使用旧的已弃用的 Latin-1 格式 ATOM_EXT。具有更高代码点的原子将使用 ATOM_UTF8_EXTSMALL_ATOM_UTF8_EXT 以 UTF-8 编码。

原子中允许的最大字符数为 255。在 UTF-8 情况下,每个字符可能需要 4 个字节进行编码。

分布头

分布头由 erlang 分布发送,以携带有关即将到来的控制消息和潜在负载的元数据。它主要用于处理 Erlang 分布中的原子缓存。自 OTP-22 以来,它还用于将大型分布消息分段为多个较小的片段。有关分布如何使用分布头的更多信息,请参见连接节点之间协议分布协议文档中的文档。

任何 ATOM_CACHE_REF 条目,其相应的 AtomCacheReferenceIndex 在分布头之后的外部格式编码的项中,都引用分布头中进行的原子缓存引用。范围是 0 <= AtomCacheReferenceIndex < 255,即,最多可以从以下项中进行 255 个不同的原子缓存引用。

普通分布头

非分段的分布头格式如下

111原子缓存引用数/2+1 | 0N | 0
13168原子缓存引用数标志原子缓存引用

标志原子缓存引用数/2+1 字节组成,除非 原子缓存引用数0。如果 原子缓存引用数0,则省略 标志原子缓存引用。每个原子缓存引用都有一个半字节标志字段。与特定的 AtomCacheReferenceIndex 对应的标志位于标志字节编号 AtomCacheReferenceIndex/2 中。标志字节 0 是 原子缓存引用数字节之后的第一个字节。偶数 AtomCacheReferenceIndex 的标志位于最低有效半字节中,奇数 AtomCacheReferenceIndex 的标志位于最高有效半字节中。

原子缓存引用的标志字段具有以下格式

1 位3 位
新缓存条目标志段索引

最高有效位是 新缓存条目标志。如果设置,则相应的缓存引用是新的。三个最低有效位是相应原子缓存条目的 段索引。原子缓存由 8 个段组成,每个段的大小为 256,即原子缓存可以包含 2048 个条目。

另一个半字节标志字段与原子缓存引用的标志字段一起存在。当 原子缓存引用数为偶数时,此半字节是原子缓存引用之后的字节的最低有效半字节。当 原子缓存引用数为奇数时,此半字节是原子缓存引用的最后一个字节的最高有效半字节(在网络上,它将显示在最后一个缓存引用之前)。它的格式如下

3 位1 位
当前未使用长原子

该半字节中的最低有效位是标志 长原子。如果设置,则在分布头中,原子长度将使用 2 个字节而不是 1 个字节。

标志字段之后,紧随其后的是 原子缓存引用。第一个 原子缓存引用是对应于 AtomCacheReferenceIndex 0 的引用。较高的索引依次递增,直到索引 原子缓存引用数 - 1

如果已为下一个 原子缓存引用设置了 新缓存条目标志,则紧随其后的是以下格式的 新原子缓存引用

11 | 2长度
内部段索引长度原子文本

内部段索引段索引一起完全标识原子缓存在原子缓存中的位置。长度原子文本组成的字节数。如果已设置标志 长原子,则长度为 2 字节大端整数,否则为 1 字节整数。当在分布握手中,两个节点之间交换了分布标志 DFLAG_UTF8_ATOMS 时,原子文本中的字符以 UTF-8 编码,否则以 Latin-1 编码。以下 缓存原子引用具有与此 新原子缓存引用相同的 段索引内部段索引,它们引用该原子,直到新的 新原子缓存引用出现,且具有相同的 段索引内部段索引

有关原子编码的更多信息,请参见上面的关于 UTF-8 编码原子的章节

如果未为下一个 原子缓存引用设置 新缓存条目标志,则紧随其后的是以下格式的 缓存原子引用

1
内部段索引

内部段索引段索引一起标识原子缓存条目在原子缓存中的位置。与此 缓存原子引用对应的原子是另一个先前传递的分布头中紧接此 缓存原子引用之前的最新 新原子缓存引用

用于分段消息的分布头

在 Erlang 节点之间发送的消息有时可能很大。自 OTP-22 以来,可以将大型消息拆分为较小的片段,以便允许在大型消息之间交错较小的消息。只有每个分布式消息message 部分可以使用分段进行拆分。因此,建议使用 OTP-22 中引入的 PAYLOAD 控制消息

仅当接收节点通过 DFLAG_FRAGMENTS 分布标志发出信号表示它支持分段分布消息时,才使用分段分布消息。

进程必须完成分段消息的发送,然后才能在同一分布通道上开始发送任何其他消息。

分段消息序列的开始如下所示

11881原子缓存引用数/2+1 | 0N | 0
13169序列 ID片段 ID原子缓存引用数标志原子缓存引用

分段消息序列的延续如下所示

1188
13170序列 ID片段 ID

起始分布头与非分段分布头非常相似。原子缓存的工作方式与普通分布头相同,并且对于整个序列都相同。添加的附加字段是序列 ID 和片段 ID。

  • 序列 ID - 序列 ID 用于唯一标识从一个进程发送到同一分布连接上的另一个进程的分段消息。这用于标识片段是哪个序列的一部分,因为同一进程可以同时接收多个序列。

    由于一个进程一次只能发送一条分段消息,因此可以使用本地 PID 作为序列 ID。

  • 片段 ID - 片段 ID 用于对序列中的片段进行编号。ID 从片段总数开始,然后递减到 1(这是最后一个片段)。因此,如果一个序列由 3 个片段组成,则起始标头中的片段 ID 将为 3,然后发送片段 2 和 1。

    片段必须以正确的顺序传递,因此如果使用了无序分布载体,则必须先对其进行排序,然后再传递给 Erlang 运行时。

示例

例如,假设我们要使用 128 的片段大小将 {call, <0.245.2>, {set_get_state, <<0:1024>>}} 发送到注册进程 reg。要发送此消息,我们需要一个分布头、原子缓存更新、控制消息(在本例中为 {6, <0.245.2>, [], reg})以及最终的实际消息。这将全部编码为

131,69,0,0,2,168,0,0,5,83,0,0,0,0,0,0,0,2,               %% Header with seq and frag id
5,4,137,9,10,5,236,3,114,101,103,9,4,99,97,108,108,      %% Atom cache updates
238,13,115,101,116,95,103,101,116,95,115,116,97,116,101,
104,4,97,6,103,82,0,0,0,0,85,0,0,0,0,2,82,1,82,2,        %% Control message
104,3,82,3,103,82,0,0,0,0,245,0,0,0,2,2,                 %% Actual message using cached atoms
104,2,82,4,109,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

131,70,0,0,2,168,0,0,5,83,0,0,0,0,0,0,0,1,               %% Cont Header with seq and frag id
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,               %% Rest of payload
0,0,0,0

让我们将其分解为各个组成部分。首先是分布头标签,以及序列 ID 和片段 ID 2。

131,69,                   %% Start fragment header
0,0,2,168,0,0,5,83,       %% The sequence ID
0,0,0,0,0,0,0,2,           %% The fragment ID

然后是原子缓存的更新。

5,4,137,9,  %% 5 atoms and their flags
10,5,       %% The already cached atom ids
236,3,114,101,103,  %% The atom 'reg'
9,4,99,97,108,108,  %% The atom 'call'
238,13,115,101,116,95,103,101,116,95,115,116,97,116,101, %% The atom 'set_get_state'

第一个字节表示我们有 5 个原子属于缓存。接下来是三个字节,表示原子缓存引用标志。每个标志使用 4 位,因此以十进制字节形式读取有点困难。以二进制半字节形式,它们看起来像这样:

0000, 0100, 1000, 1001, 1001

由于缓存中前两个原子的高位未设置,我们知道它们已在缓存中,因此不必再次发送(这是接收节点和发送节点的节点名称)。然后是必须发送的原子及其段 ID。

接下来是原子列表,首先是 10 和 5,它们是已缓存原子的原子引用。然后发送新原子。

当原子缓存正确设置后,将发送控制消息。

104,4,97,6,103,82,0,0,0,0,85,0,0,0,0,2,82,1,82,2,

请注意,到目前为止,不允许对消息进行分片。整个原子缓存和控制消息必须是起始片段的一部分。在控制消息之后,消息的有效负载将使用 128 个字节发送。

104,3,82,3,103,82,0,0,0,0,245,0,0,0,2,2,
104,2,82,4,109,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

由于有效负载大于 128 字节,因此将其拆分为两个片段。第二个片段没有任何原子缓存更新指令,因此简单得多。

131,70,0,0,2,168,0,0,5,83,0,0,0,0,0,0,0,1, %% Continuation dist header 70 with seq and frag id
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, %% remaining payload
0,0,0,0

注意

128 的片段大小仅用作示例。发送分片消息时可以使用任何片段大小。

ATOM_CACHE_REF

11
82AtomCacheReferenceIndex

引用分布头中具有AtomCacheReferenceIndex的原子。

SMALL_INTEGER_EXT

11
97Int

无符号 8 位整数。

INTEGER_EXT

14
98Int

带符号的 32 位整数,采用大端格式。

FLOAT_EXT

131
99浮点数字符串

有限浮点数(即不是 inf、-inf 或 NaN)以字符串格式存储。 sprintf 中用于格式化浮点数的格式为“%.20e”(分配的字节数比必要的要多)。要解包浮点数,请使用 sscanf 和格式“%lf”。

此术语用于外部格式的次要版本 0;它已被NEW_FLOAT_EXT取代。

PORT_EXT

1N41
102节点ID创建

NEW_PORT_EXT相同,只是Creation字段只有一个字节,并且只有两位有效,其余的为 0。

NEW_PORT_EXT

1N44
89节点ID创建

V4_PORT_EXT相同,只是ID字段只有四个字节。只有 28 位有效;其余的为 0。

NEW_PORT_EXT是在 OTP 19 中引入的,但仅用于解码和回显。不为本地端口编码。

在 OTP 23 中,分布标志DFLAG_BIG_CREATION成为强制性的。现在所有端口都使用NEW_PORT_EXT编码,即使是从旧节点接收为PORT_EXT的外部端口也是如此。

V4_PORT_EXT

1N84
120节点ID创建

编码一个端口标识符(从erlang:open_port/2获得)。Node是发起节点,编码为一个原子ID是一个 64 位大端无符号整数。Creation的工作方式与NEW_PID_EXT中的一样。不允许跨节点边界进行端口操作。

在 OTP 26 中,分布标志DFLAG_V4_NC以及V4_PORT_EXT成为强制性的,接受解码和回显完整的 64 位端口。

PID_EXT

1N441
103节点ID序列号创建

NEW_PID_EXT相同,只是Creation字段只有一个字节,并且只有两位有效,其余的为 0。

NEW_PID_EXT

1N444
88节点ID序列号创建

编码一个 Erlang 进程标识符对象。

  • Node - 发起节点的名称,编码为一个原子

  • ID - 一个 32 位大端无符号整数。

  • Serial - 一个 32 位大端无符号整数。

  • Creation - 一个 32 位大端无符号整数。来自同一节点实例的所有标识符必须具有相同的Creation值。这使得可以将来自旧(崩溃)节点的标识符与新节点的标识符分开。值零是保留的,必须避免用于正常操作。

NEW_PID_EXT是在 OTP 19 中引入的,但仅用于解码和回显。不为本地进程编码。

在 OTP 23 中,分布标志DFLAG_BIG_CREATION成为强制性的。现在所有 pid 都使用NEW_PID_EXT编码,即使是从旧节点接收为PID_EXT的外部 pid 也是如此。

在 OTP 26 中,分布标志DFLAG_V4_NC成为强制性的,接受解码和回显完整的 64 位 pid。

SMALL_TUPLE_EXT

11N
104元数元素

编码一个元组。Arity字段是一个无符号字节,确定Elements部分中有多少个元素。

LARGE_TUPLE_EXT

14N
105元数元素

SMALL_TUPLE_EXT相同,只是Arity是一个无符号的 4 字节整数,采用大端格式。

MAP_EXT

14N
116元数键值对

编码一个映射。Arity字段是一个无符号的 4 字节整数,采用大端格式,确定映射中键值对的数量。键值对 (Ki => Vi) 在Pairs部分中按以下顺序编码:K1, V1, K2, V2,..., Kn, Vn不允许在同一映射中出现重复的键。

Erlang/OTP 17.0 起

NIL_EXT

1
106

空列表的表示形式,即 Erlang 语法 []

STRING_EXT

12长度
107长度字符

字符串没有对应的 Erlang 表示形式,但它是为了在分发中更有效地发送字节列表(范围为 0-255 的整数)而进行的优化。由于Length字段是一个无符号的 2 字节整数(大端),因此实现必须确保长度超过 65535 个元素的列表编码为LIST_EXT

LIST_EXT

14
108长度元素尾部

LengthElements部分中跟随的元素数量。Tail是列表的最终尾部;对于正确的列表,它是NIL_EXT,但如果列表是不正确的(例如,[a|b]),则可以是任何类型。

BINARY_EXT

14长度
109长度数据

二进制文件是使用位语法表达式或erlang:list_to_binary/1erlang:term_to_binary/1或从二进制端口输入生成的。Len长度字段是一个无符号的 4 字节整数(大端)。

SMALL_BIG_EXT

111n
110n符号d(0) ... d(n-1)

大数以一元形式存储,带有一个Sign字节,即,如果大数是正数,则为 0,如果是负数,则为 1。数字以最低有效字节首先存储。要计算整数,可以使用以下公式:

B = 256
(d0*B^0 + d1*B^1 + d2*B^2 + ... d(N-1)*B^(n-1))

LARGE_BIG_EXT

141n
111n符号d(0) ... d(n-1)

SMALL_BIG_EXT相同,只是长度字段是一个无符号的 4 字节整数。

REFERENCE_EXT(已弃用)

1N41
101节点ID创建

NEW_REFERENCE_EXT相同,只是ID只有一个字 (Len = 1)。

NEW_REFERENCE_EXT

12N1N'
114长度节点创建ID ...

NEWER_REFERENCE_EXT 相同,除了

  • ID - 在 ID 的第一个字(4 字节)中,只有 18 位是有效的,其余位必须为 0。

  • Creation - 仅 1 字节长,且只有 2 位是有效的,其余位必须为 0。

NEWER_REFERENCE_EXT

12N4N'
90长度节点创建ID ...

编码一个使用 erlang:make_ref/0 生成的引用项。

  • Node - 发起节点的名称,编码为一个原子

  • Len - 一个 16 位大端无符号整数,不大于 5。

  • ID - 一个由 Len 个大端无符号整数组成的序列(每个 4 字节,因此 N' = 4 * Len),但应被视为未解释的数据。

  • Creation - 工作方式与 NEW_PID_EXT 中相同。

NEWER_REFERENCE_EXT 在 OTP 19 中引入,但仅用于解码和回显。不为本地引用编码。

在 OTP 23 中,分布标志 DFLAG_BIG_CREATION 成为强制性的。现在所有引用都使用 NEWER_REFERENCE_EXT 编码,即使是从较旧的节点以 NEW_REFERENCE_EXT 接收的外部引用也是如此。

在 OTP 26 中,分布标志 DFLAG_V4_NC 成为强制性的。现在引用可以包含最多 5 个 ID 字。

FUN_EXT(已移除)

14N1N2N3N4N5
117NumFreePidModuleIndexUniqFree vars ...

自 OTP R8 之后不再发出,自 OTP 23 之后不再解码。

NEW_FUN_EXT

1411644N1N2N3N4N5
112Size元数UniqIndexNumFreeModuleOldIndexOldUniqPidFree Vars

这是内部 fun 的编码:fun F/Afun(Arg1,..) -> ... end

  • Size - 总字节数,包括字段 Size

  • Arity - 实现 fun 的函数的元数。

  • Uniq - Beam 文件重要部分的 16 字节 MD5 值。

  • Index - 一个索引号。模块中的每个 fun 都有一个唯一的索引。Index 以大端字节顺序存储。

  • NumFree - 自由变量的数量。

  • Module - 实现 fun 的模块,编码为原子

  • OldIndex - 使用 SMALL_INTEGER_EXTINTEGER_EXT 编码的整数。通常是模块 fun 表中的一个小索引。

  • OldUniq - 使用 SMALL_INTEGER_EXTINTEGER_EXT 编码的整数。Uniq 是 fun 的语法树的哈希值。

  • Pid - PID_EXT 中的进程标识符。表示创建 fun 的进程。

  • Free vars - NumFree 个术语,每个术语都根据其类型进行编码。

EXPORT_EXT

1N1N2N3
113ModuleFunction元数

此项是外部 fun 的编码:fun M:F/A

ModuleFunction 编码为原子

Arity 是一个使用 SMALL_INTEGER_EXT 编码的整数。

BIT_BINARY_EXT

141长度
77长度Bits数据

此项表示一个位串,其位长度不必是 8 的倍数。Len 字段是一个 4 字节无符号整数(大端)。 Bits 字段是数据字段中最后一个字节中使用的位数 (1-8),从最高有效位到最低有效位计数。

NEW_FLOAT_EXT

18
70IEEE float

一个有限的浮点数(即不是 inf、-inf 或 NaN)以 8 字节大端 IEEE 格式存储。

此项用于外部格式的次版本 1。

ATOM_UTF8_EXT

12长度
118长度AtomName

原子以 2 字节大端无符号长度存储,后跟 Len 字节,其中包含以 UTF-8 编码的 AtomName

有关更多信息,请参见本页面开头的关于编码原子的部分

SMALL_ATOM_UTF8_EXT

11长度
119长度AtomName

原子以 1 字节无符号长度存储,后跟 Len 字节,其中包含以 UTF-8 编码的 AtomName。可以使用 ATOM_UTF8_EXT 表示以 UTF-8 编码的较长原子。

有关更多信息,请参见本页面开头的关于编码原子的部分

ATOM_EXT(已弃用)

12长度
100长度AtomName

原子以 2 字节大端无符号长度存储,后跟 Len 个 8 位 Latin-1 字符,这些字符构成 AtomNameLen 的最大允许值为 255。

SMALL_ATOM_EXT(已弃用)

11长度
115长度AtomName

原子以 1 字节无符号长度存储,后跟 Len 个 8 位 Latin-1 字符,这些字符构成 AtomName

注意

SMALL_ATOM_EXT 在 ERTS 5.7.2 中引入,并且需要在 分布握手中交换分布标志 DFLAG_SMALL_ATOM_TAGS

LOCAL_EXT

1...
121...

标记这是在替代的本地外部术语格式上编码的,该格式仅供特定的本地解码器解码。从此处开始的字节可能包含任何未指定的术语编码类型。用户有责任仅尝试解码与匹配编码器生成的本地外部术语格式上的术语。

当将 local 选项传递给 term_to_binary/2 时,Erlang 运行时系统在编码本地外部术语格式时使用此标记,但其他编码器也可以使用它,提供类似的功能。Erlang 运行时系统在 LOCAL_EXT 标记之后立即添加哈希值,该哈希值在解码时进行验证,以验证编码器和解码器是否匹配,这可能是一个很好的做法。这将很可能捕获用户犯的错误,但不能保证,也不打算防止对本地外部术语格式进行故意伪造的编码进行解码。

LOCAL_EXT 在 OTP 26.0 中引入。