在线文档教程
Erlang 20

12. Distribution Protocol

12分发协议

这个描述还远远没有完成。如果协议更新,它将被更新。但是,从Erlang节点到Erlang端口映射程序守护进程(EPMD)以及Erlang节点之间的协议自多年以来一直保持稳定。

分发协议可以分为四个部分:

  • 低级套接字连接(1)

  • 握手,交换节点名称和验证(2)

  • 验证(完成net_kernel(3))(3)

  • 已连接(4)

节点通过EPMD(在另一个主机)获取另一个节点的端口号以发起连接请求。

对于运行分布式Erlang节点的每个主机,还将运行EPMD。作为Erlang节点启动的结果,EPMD可以显式启动或自动启动。

默认情况下,EPMD在端口4369上侦听。

(3)和(4)在同一级别执行,但net_kernel如果使用无效cookie进行通信(1秒后),则断开其他节点的连接。

所有多字节字段中的整数按大端顺序排列.

警告

Erlang分发协议本身并不安全,并不是为了这么做。为了获得安全的分布,分布式节点应该被配置为使用tls上的分布。有关Using SSL for Erlang Distribution如何设置安全分布式节点的详细信息,请参见用户指南。

12.1 EPMD协议

EPMD所提供的请求摘要如下。

图12.1:EPMD请求摘要

每次请求*_REQ前面有一个2字节长的字段。因此,总的请求格式如下:

2n
LengthRequest

在EPMD中注册节点

当分布式节点启动时,它将自己注册到EPMD中。下面ALIVE2_REQ描述的消息从节点发送到EPMD。EPMD的回应是ALIVE2_RESP

1211222Nlen2Elen
120PortNoNodeTypeProtocolHighestVersionLowestVersionNlenNodeNameElenExtra

PortNo

节点接受连接请求的端口号。

NodeType

77 =正常Erlang节点,72 =隐藏节点(C节点),...

Protocol

0 = TCP / IPv4,...

HighestVersion

此节点可以处理的最高分发版本。Erlang/OTP R6B及更高版本中的值是5。

LowestVersion

此节点可以处理的最低分发版本。Erlang/OTP R6B及更高版本中的值是5。

Nlen

字段的长度(以字节为单位)NodeName

NodeName

节点名为utf-8编码字符串。Nlen字节。

Elen

场的长度Extra

Extra

额外字段Elen字节。

只要节点是分布式节点,就必须保持为EPMD创建的连接。当连接关闭时,节点将自动从EPMD注销。

回应信息ALIVE2_RESP如下:

112
121ResultCreation

结果=0->确定,结果>0->错误。

从EPMD注销节点

一个节点通过关闭到节点注册时建立的EPMD的TCP连接从EPMD注销自己。

获取另一个节点的分发端口

当一个节点想要连接到另一个节点时,它首先PORT_PLEASE2_REQ向节点驻留的主机上的EPMD发出请求,以获取该节点监听的分发端口。

1N
122NodeName

其中N=Length-1。

11
119Result

11211222Nlen2Elen
119ResultPortNoNodeTypeProtocolHighestVersionLowestVersionNlenNodeNameElenExtra

如果Result> 0,数据包只包含[119, Result]。

EPMD在发送信息时关闭套接字。

从EPMD获取所有注册姓名

这个请求是通过Erlang函数使用的net_adm:names/1,2。TCP连接向EPMD打开并发送此请求。

*。(鼓掌)

一百一十

对a的反应NAMES_REQ如下:

4
EPMDPortNoNodeInfo*

NodeInfo是为每个活动节点写的字符串。全部NodeInfo写完后,EPMD关闭连接。

NodeInfo如Erlang所述:

io:format("name ~ts at port ~p~n", [NodeName, Port]).

转储EPMD中的所有数据

这个请求没有被真正使用,它被认为是一个调试功能。

|:----|

| 100 |

答案DUMP_REQ如下:

4
EPMDPortNoNodeInfo*

NodeInfo是为保存在EPMD中的每个节点编写的字符串。全部NodeInfo写完后,EPMD关闭连接。

NodeInfo如Erlang所述:

io:format("active name ~ts at port ~p, fd = ~p~n", [NodeName, Port, Fd]).

io:format("old/unused name ~ts at port ~p, fd = ~p ~n", [NodeName, Port, Fd]).

Kill EPMD

该请求会终止正在运行的EPMD。它几乎从未使用过。

| 1 |

|:----|

| 107 |

答案KILL_REQ如下:

| 2 |

|:----|

| OKString |

OKString“OK” 。

STOP_REQ(未使用)

1n
115NodeName

其中n=Length-1。

Erlang的当前实现并不关心与EPMD的连接是否中断。

对a的反应STOP_REQ如下:

| 7 |

|:----|

| OKString |

OKString“停止” 。

负面反应可如下所示:

| 7 |

|:----|

| NOKString |

NOKString“NOEXIST”。

12.2分配握手

本节介绍在Erlang / OTP R6中引入的分发握手协议。此说明先前$ERL_TOP/lib/kernel/internal_doc/distribution_handshake.txt已在此处,并且或多或少已在此处进行复制和“格式化”。自1999年以来几乎没有变化,但自那时以来握手情况没有太大变化。

一般

TCP / IP分发使用期望基于连接的协议的握手,即协议在握手过程之后不包括任何身份验证。

这并不完全安全,因为它容易遭受收购攻击,但这是公平安全和性能之间的折衷。

cookies不会以明文形式发送,握手过程期望客户端(被叫A)成为第一个证明它能够生成足够摘要的客户端。摘要是使用MD5消息摘要算法生成的,挑战期望是随机数。

定义

挑战是大端顺序的32位整数。该函数下面gen_challenge()返回一个随机的32位整数作为挑战。

摘要是与cookie(作为文本)连接的挑战(作为文本)的(16字节)MD5散列。以下,该功能gen_digest(Challenge, Cookie)如上所述生成摘要。

一个out_cookie是在输出通信用于某个节点的饼干,使Aout_cookie用于B将与对应Bin_cookieA,反之亦然。Aout_cookieBAin_cookieB需求一样。下面的函数out_cookie(Node)返回当前节点的out_cookieNode

一个in_cookie是预期与我们交流时,由另一个节点所使用的cookie时,使Ain_cookieB与对应Bout_cookieA。下面的函数in_cookie(Node)返回当前节点的in_cookieNode

Cookie是可视为密码的文本字符串。

握手中的每条消息都以一个16位的大端整数开始,它包含消息长度(不包括两个起始字节)。在二郎这相当于选项{packet, 2}gen_tcp(3)。请注意,握手后,分配将切换到4字节的数据包标头。

详细的握手

设想两个节点,A即启动握手并B接受连接。

1) connect/accept

A连接到B通过TCP/IP和B接受连接。

2)send_name**/**receive_name

A发送一个初始标识B,接收该消息。该消息看起来如下(每个“方块”是一个字节并且分组头部被移除):

+---+--------+--------+-----+-----+-----+-----+-----+-----+-...-+-----+ |'n'|Version0|Version1|Flag0|Flag1|Flag2|Flag3|Name0|Name1| ... |NameN| +---+--------+--------+-----+-----+-----+-----+-----+-----+-... +-----+

'n'是消息标签。'版本0'和'版本1'是A根据EPMD的信息选择的分发版本。(16位big-endian)'Flag0'...'Flag3'是能力标志,能力定义在$ERL_TOP/lib/kernel/include/dist.hrl。(32位big-endian)'Name0'...'NameN'是整个节点的名称A,作为一个字节串(数据包长度表示它有多长)。

3)recv_status**/**send_status

B发送一条状态消息给A,指示连接是否被允许。定义了以下状态码:

ok

握手还会继续。

ok_simultaneous

握手将继续,但A被告知B有另一个正在进行的连接尝试将被关闭(同时连接A的名称大于B名字,相比字面意思)。

nok

握手不会继续,因为B握手已经进行了,它本身已经开始了(同时连接B的名字大于A's')。

not_allowed

该连接因某些(未指定的)安全原因而被禁止。

alive

与该节点的连接已处于活动状态,这意味着该节点A感到困惑,或者该名称的前一个节点的TCP连接故障尚未到达节点B。请参阅下面的步骤3B

状态消息的格式如下:

+---+-------+-------+-...-+-------+ |'s'|Status0|Status1| ... |StatusN| +---+-------+-------+-...-+-------+

's'是消息标签。'Status0'...'StatusN'是一个字符串(未终止)的状态。

3B)send_status**/**recv_status

如果状态为alive,则节点A以另一个包含其中的状态消息来回答true,这意味着连接将继续(来自此节点的旧连接中断),或者false,这意味着连接将被关闭(连接尝试是一个错误。

4)recv_challenge**/**send_challenge

如果状态是ok或者ok_simultaneous,握手继续B发送A另一条消息,挑战。挑战包含相同类型的“姓名”的消息最初由发送信息AB,再加上一个32位的挑战:

+---+--------+--------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-...-+-----+ |'n'|Version0|Version1|Flag0|Flag1|Flag2|Flag3|Chal0|Chal1|Chal2|Chal3|Name0|Name1| ... |NameN| +---+--------+--------+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-... +-----+

'Chal0'...'Chal3'是作为32位大端整数的挑战,其他字段是B版本,标志和完整节点名称。

5)send_challenge_reply**/**recv_challenge_reply

现在A已经产生了一个摘要和自己的挑战。这些邮件一起发送到B

+---+-----+-----+-----+-----+-----+-----+-----+-----+-...-+------+ |'r'|Chal0|Chal1|Chal2|Chal3|Dige0|Dige1|Dige2|Dige3| ... |Dige15| +---+-----+-----+-----+-----+-----+-----+-----+-----+-...-+------+

'r'是标签。'Chal0'...'Chal3'是处理A的挑战B。'Dige0'...'Dige15'是根据上一步发送A的挑战构建的摘要B

6)recv_challenge_ack**/**send_challenge_ack

B检查收到的摘要A是否正确,并根据收到的质询生成摘要A。摘要然后被发送到A。消息如下:

+---+-----+-----+-----+-----+-...-+------+ |'a'|Dige0|Dige1|Dige2|Dige3| ... |Dige15| +---+-----+-----+-----+-----+-...-+------+

'a'是标签。“Dige0” ...“Dige15”是计算的摘要B进行A的挑战。

7) check

A检查摘要B并且连接已经结束。

符号学视图

A (initiator) B (acceptor) TCP connect ------------------------------------> TCP accept send_name --------------------------------------> recv_name <---------------------------------------------- send_status recv_status (if status was 'alive' send_status - - - - - - - - - - - - - - - - - -> recv_status) ChB = gen_challenge() (ChB) <---------------------------------------------- send_challenge recv_challenge ChA = gen_challenge(), OCA = out_cookie(B), DiA = gen_digest(ChB, OCA) (ChA, DiA) send_challenge_reply ---------------------------> recv_challenge_reply ICB = in_cookie(A), check: DiA == gen_digest (ChB, ICB)? - if OK: OCB = out_cookie(A), DiB = gen_digest (ChA, OCB) (DiB) <----------------------------------------------- send_challenge_ack recv_challenge_ack DONE ICA = in_cookie(B), - else: check: CLOSE DiB == gen_digest(ChA, ICA)? - if OK: DONE - else: CLOSE

分布旗

定义了以下功能标志:

-define(DFLAG_PUBLISHED,16#1).

该节点将被发布,并成为全局命名空间的一部分。

-define(DFLAG_ATOM_CACHE,16#2).

节点实现原子缓存(过时)。

-define(DFLAG_EXTENDED_REFERENCES,16#4).

该节点实现扩展(3×32位)参考。这是今天需要的。如果不存在,则连接被拒绝。

-define(DFLAG_DIST_MONITOR,16#8).

节点实现分布式进程监控。

-define(DFLAG_FUN_TAGS,16#10).

该节点在分发协议中使用单独的标签(lambda)。

-define(DFLAG_DIST_MONITOR_NAME,16#20).

节点实现分布式命名进程监控。

-define(DFLAG_HIDDEN_ATOM_CACHE,16#40).

(隐藏)节点实现原子缓存(过时)。

-define(DFLAG_NEW_FUN_TAGS,16#80).

节点理解新的有趣标记。

-define(DFLAG_EXTENDED_PIDS_PORTS,16#100).

该节点可以处理扩展的pid和端口。这是今天需要的。如果不存在,则连接被拒绝。

-define(DFLAG_EXPORT_PTR_TAG,16#200).-define(DFLAG_BIT_BINARIES,16#400).-define(DFLAG_NEW_FLOATS,16#800).

节点理解新的浮点格式。

-define(DFLAG_UNICODE_IO,16#1000).-define(DFLAG_DIST_HDR_ATOM_CACHE,16#2000).

节点在分发头中实现原子缓存。

-define(DFLAG_SMALL_ATOM_TAGS, 16#4000).

节点理解SMALL_ATOM_EXT标签。

-define(DFLAG_UTF8_ATOMS, 16#10000).

节点理解UTF-8编码原子。

连接节点间的12.3协议

从ERTS 5.7.2开始,运行时系统在握手阶段传递一个分配标志,使distribution header所有传递的消息都可以使用。在这种情况下,节点之间传递的消息具有以下格式:

4dnm
LengthDistributionHeaderControlMessageMessage

Length

等于d+n+m。

ControlMessage

使用Erlang的外部格式传递的元组。

Message

使用'!'发送到另一个节点的消息 (以外部格式)。注意,Message它只与一个ControlMessage编码send('!')结合使用。

注意到the version number is omitted from the terms that follow a distribution header

ERTS版本早于5.7.2的节点不会传递启用分发标头的分发标志。在这种情况下,节点之间传递的消息具有以下格式:

41nm
LengthTypeControlMessageMessage

Length

等于1+n+m。

Type

等于112(通过)。

ControlMessage

使用Erlang外部格式传递的元组。

Message

使用'!'发送到另一个节点的消息 (以外部格式)。注意,Message它只与一个ControlMessage编码send('!')结合使用。

ControlMessage是一个元组,第一个元素指示它编码的是哪个分布式操作:

LINK

{1, FromPid, ToPid}

SEND

{2, Unused, ToPid}

随后Message

Unused是为了向后兼容性而保存的。

EXIT

{3, FromPid, ToPid, Reason}

UNLINK

{4, FromPid, ToPid}

NODE_LINK

{5}

REG_SEND

{6, FromPid, Unused, ToName}

随后Message

Unused是为了向后兼容性而保存的。

GROUP_LEADER

{7, FromPid, ToPid}

EXIT2

{8, FromPid, ToPid, Reason}

12.4 New Ctrlmessages for distrvsn = 1 (Erlang/OTP R4)

SEND_TT

{12, Unused, ToPid, TraceToken}

随后Message

Unused是为了向后兼容性而保存的。

EXIT_TT

{13, FromPid, ToPid, TraceToken, Reason}

REG_SEND_TT

{16, FromPid, Unused, ToName, TraceToken}

随后Message

Unused 保持向后兼容性。

EXIT2_TT

{18, FromPid, ToPid, TraceToken, Reason}

12.5用于分发的新CtrlMessage=2

distrvsn2从未使用过。

12.6 distrvsn = 3的新的Ctrl消息(Erlang/OTP R5C)

没有,但版本号增加了。

12.7条用于分发的新CtrlMessage=4%28 Erlang/OTP R6%29

这些只能被Erlang节点识别,而不能被隐藏节点识别。

MONITOR_P

{19, FromPid, ToProc, Ref},其中FromPid=监控进程和ToProc=监控进程pid或名称(原子)

DEMONITOR_P

{20, FromPid, ToProc, Ref},其中FromPid=监控进程和ToProc=监控进程pid或名称(原子)

我们包括FromPid以防万一我们想要跟踪这一点。

MONITOR_P_EXIT

{21, FromProc, ToPid, Ref, Reason},其中FromProc=监控的进程PID或名称(原子),ToPid=监控进程,以及Reason= 监控进程的退出原因