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字节长的字段。因此,总的请求格式如下:
2 | n |
---|---|
Length | Request |
在EPMD中注册节点
当分布式节点启动时,它将自己注册到EPMD中。下面ALIVE2_REQ
描述的消息从节点发送到EPMD。EPMD的回应是ALIVE2_RESP
。
1 | 2 | 1 | 1 | 2 | 2 | 2 | Nlen | 2 | Elen |
---|---|---|---|---|---|---|---|---|---|
120 | PortNo | NodeType | Protocol | HighestVersion | LowestVersion | Nlen | NodeName | Elen | Extra |
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
如下:
1 | 1 | 2 |
---|---|---|
121 | Result | Creation |
结果=0->确定,结果>0->错误。
从EPMD注销节点
一个节点通过关闭到节点注册时建立的EPMD的TCP连接从EPMD注销自己。
获取另一个节点的分发端口
当一个节点想要连接到另一个节点时,它首先PORT_PLEASE2_REQ
向节点驻留的主机上的EPMD发出请求,以获取该节点监听的分发端口。
1 | N |
---|---|
122 | NodeName |
其中N=Length
-1。
1 | 1 |
---|---|
119 | Result |
或
1 | 1 | 2 | 1 | 1 | 2 | 2 | 2 | Nlen | 2 | Elen |
---|---|---|---|---|---|---|---|---|---|---|
119 | Result | PortNo | NodeType | Protocol | HighestVersion | LowestVersion | Nlen | NodeName | Elen | Extra |
如果Result> 0,数据包只包含[119, Result]。
EPMD在发送信息时关闭套接字。
从EPMD获取所有注册姓名
这个请求是通过Erlang函数使用的net_adm:names/1,2
。TCP连接向EPMD打开并发送此请求。
一
*。(鼓掌)
一百一十
对a的反应NAMES_REQ
如下:
4 | |
---|---|
EPMDPortNo | NodeInfo* |
NodeInfo
是为每个活动节点写的字符串。全部NodeInfo
写完后,EPMD关闭连接。
NodeInfo
如Erlang所述:
io:format("name ~ts at port ~p~n", [NodeName, Port]).
转储EPMD中的所有数据
这个请求没有被真正使用,它被认为是一个调试功能。
一
|:----|
| 100 |
答案DUMP_REQ
如下:
4 | |
---|---|
EPMDPortNo | NodeInfo* |
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(未使用)
1 | n |
---|---|
115 | NodeName |
其中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
是在输出通信用于某个节点的饼干,使A
的out_cookie
用于B
将与对应B
的in_cookie
对A
,反之亦然。A
的out_cookie
对B
和A
的in_cookie
对B
需求不
一样。下面的函数out_cookie(Node)
返回当前节点的out_cookie
对Node
。
一个in_cookie
是预期与我们交流时,由另一个节点所使用的cookie时,使A
的in_cookie
对B
与对应B
的out_cookie
对A
。下面的函数in_cookie(Node)
返回当前节点的in_cookie
对Node
。
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
另一条消息,挑战。挑战包含相同类型的“姓名”的消息最初由发送信息A
到B
,再加上一个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
所有传递的消息都可以使用。在这种情况下,节点之间传递的消息具有以下格式:
4 | d | n | m |
---|---|---|---|
Length | DistributionHeader | ControlMessage | Message |
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的节点不会传递启用分发标头的分发标志。在这种情况下,节点之间传递的消息具有以下格式:
4 | 1 | n | m |
---|---|---|---|
Length | Type | ControlMessage | Message |
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
distrvsn
2从未使用过。
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
= 监控进程的退出原因