ZeroMQ Message Transport Protocol(ZMTP) 是一个传输层协议。其通过诸如 TCP 等运输层协议在两个对等点之间交换消息。此文档描述了 ZMTP 3.1。此版本添加了 JOIN, LEAVE, SUBSCRIBE, CANCEL, PING 和 PONG 命令和端点资源。
原文地址: 37/ZMTP
目标
ZMTP 解决了我们使用 TCP 交换信息时的遇到的问题:
TCP 是字节流协议,没有分界符,但是我们希望发送和接收消息。因此 ZMTP 写入和读取由 size 和 body 构成了帧。
我们需要在每个帧中携带 metadata(例如这个帧是否是多部分消息的一部分)。ZMTP 在每帧的 metadata 中添加一个 flag 字段。
我们需要能够与旧版本实现进行通信,以便我们的框架可以在不破坏现有实现的情况下发展。ZMTP 定义了一个 greeting,用于宣布版本号,并指定版本协商的方法。
我们需要安全性,以便对等方可以确定与他们通信的对等方的身份,并且消息不会被第三方篡改或检查。ZMTP 定义了一个 安全握手,允许对等方创建安全连接。
我们需要一系列安全协议,从明文(没有安全性,但速度快)到完全身份验证和加密(安全但速度慢)。此外,随着时间的推移,我们需要自由地添加新的安全协议。ZMTP 定义了一种让对等方就可扩展的安全机制达成一致的方式。
我们需要一种方法来在安全握手后携带有关连接的元数据。ZMTP 定义了一组标准的元数据属性(套接字类型、标识等),这些属性在安全机制之后对等方交换。
我们希望允许多个任务共享单个外部唯一接口和端口,以降低系统管理成本。
我们需要以一种易于团队在任何平台和任何语言上实施的方式写下这些解决方案。因此,ZMTP 被指定为正式协议(本文档),并在自由许可下提供给团队。
我们需要保证人们不会创建 ZMTP 的私有分支,从而破坏互操作性。因此,ZMTP 在 GPLv3 下获得许可,因此任何派生版本也必须提供给实现它的软件的用户。
自 ZMTP 3.0 后的更改
ZMTP 添加了 resource 元数据属性以允许任意数量的 task 共享单个接口和端口。
相关规范:
http://rfc.zeromq.org/spec:23/ZMTP 定义了 ZMTP v3.0。
http://rfc.zeromq.org/spec:24/ZMTP-PLAIN 定义了 ZMTP-PLAIN 安全机制。
http://rfc.zeromq.org/spec:25/ZMTP-CURVE 定义了 ZMTP-CURVE 安全机制。
http://rfc.zeromq.org/spec:26/CURVEZMQ 定义了 CurveZMQ 认证和加密机制。
http://rfc.zeromq.org/spec:27/ZAP 定义了 ZeroMQ 认证协议。
http://rfc.zeromq.org/spec:28/REQREP 定义了 REQ, REP, DEALER, ROUTER 套接字语义。
http://rfc.zeromq.org/spec:29/PUBSUB 定义了 PUB, XPUB, SUB, XSUB 套接字语义。
http://rfc.zeromq.org/spec:30/PIPELINE 定义了 PUSH and PULL 套接字语义。
http://rfc.zeromq.org/spec:31/EXPAIR 定义了 exclusive PAIR 套接字语义。
http://rfc.zeromq.org/spec:41/CLIENTSERVER 定义了 CLIENT 和 SERVER 套接字语义。
http://rfc.zeromq.org/spec:48/RADIODISH 定义了 RADIO 和 DISH 套接字语义。
http://rfc.zeromq.org/spec:49/SCATTERGATHER 定义了 SCATTER 和 GATHER 套接字语义。
http://rfc.zeromq.org/spec:51/P2P 定义了 PEER 套接字语义。
http://rfc.zeromq.org/spec:52/CHANNEL 定义了 CHANNEL 套接字语义。
实现
总体行为
ZMTP 连接经历以下主要阶段:
两个对等方通过相互发送数据并继续讨论或关闭连接,就连接的版本和安全机制达成一致。
两个对等方通过交换零个或多个命令来握手安全机制。如果安全握手成功,对等方将继续讨论,否则一个或两个对等方将关闭连接。
然后,每个对等方将有关连接的其他元数据作为最终命令发送。对等方可以检查元数据,每个对等方决定是继续还是关闭连接。
然后,每个对等方都可以发送其他消息。任何一个对等方都可以随时关闭连接。
形式语法
以下 ABNF 语法定义了该协议:
; 此协议由零个或多个连接构成。
zmtp = *connection
; 一个连接是 greeting, handshark 和 traffic 的一种。
connection = greeting handshake traffic
; greeting 宣告协议细节。
greeting = signature version mechanism as-server filler
signature = %xFF padding %x7F
padding = 8OCTET ; Not significant
version = version-major version-minor
version-major = %x03
version-minor = %x01
; mechanism 是一个由空字符填充的字符串。
mechanism = 20mechanism-char
mechanism-char = "A"-"Z" | DIGIT
| "-" | "_" | "." | "+" | %x0
; 此对等点是否在安全握手中充当服务器?
as-server = %x00 | %x01
; 将 greeting 填充到 64B。
filler = 31%x00 ; 31 zero octets
; handshake 至少由一个命令组成。
; 实际的语法依赖于安全机制。
handshake = 1*command
; traffic 由 command 和 message 交叉混成。
traffic = *(command | message)
; command 是单个长帧或者短帧。
command = command-size command-body
command-size = %x04 short-size | %x06 long-size
short-size = OCTET ; Body is 0 to 255 octets
long-size = 8OCTET ; Body is 0 to 2^63-1 octets
command-body = command-name command-data
command-name = short-size 1*255command-name-char
command-name-char = ALPHA
command-data = *OCTET
; message 是一个或多个帧。
message = *message-more message-last
message-more = ( %x01 short-size | %x03 long-size ) message-body
message-last = ( %x00 short-size | %x02 long-size ) message-body
message-body = *OCTET
版本协商
ZMTP 提供非对称版本协商。ZMTP 对等体可能会尝试检测和使用旧版本的协议。它还可能要求其对等方提供 ZMTP 功能。
在第一种情况下,在建立或接收连接后,对等方应向另一方发送足以触发版本检测的部分问候语。这是问候语的前 11 个八位字节(签名和主版本号)。然后,对等方应读取其他对等方发送的前 11 个八位字节的问候语,并确定是否降级。每个较旧的 ZMTP 版本的特定启发式方法在 向后互操作性 一节中进行了说明。在这种情况下,对等方可以使用填充字段进行较旧的协议检测(我们将在下面解释特定的已知情况)。
在第二种情况下,在建立或接收连接后,对等方应发送其整个问候语(64 个八位字节),并应期望匹配的 64 个八位字节问候语。在这种情况下,对等方应将填充字段设置为二进制零。
无论哪种情况,请注意:
对等方不得赋予填充字段任何含意,不得验证或解释它。
对等方必须接受更高的协议版本。也就是说,ZMTP 对等体必须接受大于或等于 3.1 的协议版本。这允许未来的实现安全地与当前实现进行互操作。
当对等或更高协议对等方通信时,对等方应始终使用自己的协议(包括组帧)。
对等体可以降级其协议以与较低协议的对等体通信。
如果对等体无法降级其协议以匹配其对等体,则必须关闭连接。
拓扑学
默认情况下,ZMTP 是一种对等协议,它不区分客户端和服务器。
但是,安全机制(即扩展协议,如下所述)可以为客户端对等方和服务器对等方定义不同的角色。这种差异反映了将身份验证集中在服务器上的一般模型。
传统上,在 TCP 拓扑中,“服务器”是绑定的对等体,“客户端”是连接的对等体。ZMTP 允许这样做,但也允许客户端绑定和服务器连接的相反拓扑。
如果所选的安全机制未指定客户端和服务器角色,则 as-server 字段没有意义,并且对于所有对等方都应为零。
身份验证和加密
ZMTP 通过使用松散地基于 IETF 简单身份验证和安全层 (SASL) 的协商安全机制来提供可扩展的身份验证和机密性。对等方可以支持以下任何或所有机制:
NULL:在本文档后面指定,它不实现身份验证,也不实现加密。
PLAIN:由 rfc.zeromq.org/spec:24/ZMTP-PLAIN 指定,以明文形式实现简单的用户名和密码身份验证。
CURVE:由 rfc.zeromq.org/spec:25/ZMTP-CURVE 指定,它使用 CurveZMQ 安全协议实现完全身份验证和加密。
安全机制是一个 ASCII 字符串,根据需要填充 null 以容纳 20 个八位字节。实现可以定义自己的机制用于实现和内部使用。所有用于公共互操作性的机制都应定义为 ZMQ RFC。 机制名称应以先到先得的方式分配。机制名称应仅由大写字母 A 到 Z、数字和嵌入的连字符或下划线组成。
与 SASL 不同,对等方只宣布一种安全机制,SASL 允许服务器宣布多种安全机制。ZMTP 中的安全性是断定的,因此给定套接字上的所有对等方都具有相同的所需安全级别。这样可以防止降级攻击并简化实施。
每种安全机制都定义了一个协议,该协议由任一对等体向另一方发送的零个或多个命令组成,直到握手完成或任一对等体拒绝并关闭连接。
command 是由 8b flag 字段、大小字段和正文组成的单个帧。如果正文为 0-255 个八位字节,则该命令应使用短大小字段(%x04 后跟 1 个八位字节)。如果正文为 256 个或更多八位字节,则该命令应使用长大小字段(%x06 后跟 8 个八位字节)。
flag 八位字节和大小字段始终为明文。正文可以部分加密或完全加密。ZMTP 不定义命令的语法或语义。这些完全由安全机制协议定义。
支持的机制不被视为敏感信息。读取完整 greeting(包括机制)的对等节点也必须发送完整的问候语(包括机制)。这避免了两个对等方各自等待对方发送其问候语的其余部分的僵局。
如果对等方收到的机制与其发送的机制不完全匹配,则必须关闭连接。
错误处理
ZMTP 允许在机制握手期间使用 ERROR 命令进行显式致命错误响应。对等方应将传入的 ERROR 命令视为致命命令,并通过关闭连接而不是使用相同的安全凭据重新连接来执行操作。
实现应通过关闭连接来发出任何其他错误的信号,例如过载、暂时拒绝连接等。对等方应将意外关闭的连接视为临时错误,并应重新连接。
为避免连接风暴,对等体应在较短且可能随机的间隔后重新连接。此外,如果对等体多次重新连接,则应增加重新连接之间的延迟。各种策略是可能的。
成帧
在发送了 64B 固定大小的 greeting 后,所有的数据都以帧的形式发送,帧携带了 commands 或 messages。帧由一个 flags 字段、一个 size 字段和正文构成。帧被设计为既能有效传输小数据,也能传输非常大的数据。
一个帧的构成如下:
size 字段代表了 body 的大小。当 body 小于 255B 时,size 为 1B(短帧),否则为 8B(长帧)。
flag 字段包含了多个控制标志。bit 0 是最低有效位(最右边的位)
bit 范围 含意 7-3
保留。必须为零
2
置 1 时代表此帧为 command 帧
1
置 1 时代表此帧为长帧
0
置 1 时代表此帧为多部分帧的一部分
commands
Commands 由 ZMTP 实现使用,除了部分情景外,对用户是不可见的。Commands 总是由一个帧构成。包含了一个可打印的命令名、一个空字节分隔符和数据。
此规范定义了下面的命令:
READY 和 ERROR:用户 NULL 机制握手。阅读 NULL 安全机制 来查看细节。
ZMTP 支持可拓展的安全机制,他们可能定义他们自己的命令。安全机制可以根据需要定义任意的命令名。
messages
message 携带了用户数据,除了部分情景下一般不会被 ZMTP 创建、修改或者过滤。消息由一个或多个帧构成。一个消息要么所有的帧都到,要么一个帧也不会到。
NULL 安全机制
NULL 不实现身份验证和加密。NULL 机制不应当用于没有传输安全性的公共基础设施(例如通过 VPN)。
当对等方使用 NULL 安全机制是,as-server 必须为零。绑定的对等方为服务器,连接的对等方为客户端。
要完成 NULL 安全握手。客户端应发送 READY 命令,然后等待 READY 命令作为回复。服务器应当解析并验证 READY 命令。如果没有错误,必须发送 READY 命令作为回复。如果验证成功,则必须发送 READY 命令作为回复,如果失败,则任意一方都可以选择关闭连接。对等方可以在完成握手后立即开始发送消息。即发送和接收 READY 命令。
以下使用 ABNF 语法定义 NULL 握手机制:
null = ready *message | error
ready = command-size %d5 "READY" metadata
metadata = *property
property = name value
name = short-size 1*255name-char
name-char = ALPHA | DIGIT | "-" | "_" | "." | "+"
value = 4OCTET *OCTET ; Size in network byte order
error = command-size %d5 "ERROR" error-reason
error-reason = short-size 0*255VCHAR
message 和 command-size 由前面解释的 ZMTP 语法定义。
READY 命令的正文由一个属性列表组成,该列表由 name 和 value 组成,这些属性是大小指定的字符串。
名称应为 1 到 255 个字符。零大小的名称无效。名称的大小写(大写或小写)不应重要。
值应为 0 到 2,147,483,647(2^31-1 或 C/C++ 中的 INT32_MAX)不透明二进制数据的八位字节。允许使用零大小的值。值的语义取决于属性。值大小字段应为四个八位字节,按网络字节序排列。请注意,此大小字段在内存中大多不会对齐。
ERROR 命令的正文包含可以记录的错误原因。它没有定义的语义值。
连接元数据
安全机制提供了交换字典形式的元数据,元数据的具体编码形式取决于机制。
元数据的键是大小写不敏感的。
默认定义了下面的元数据属性:
"Socket-Type":指明了发送者的套接字类型。参阅 [套接字类型属性],发送者需要指定此属性。
"Identity":制订了发送者的套接字标识。参阅 Identity 属性,发送者 可能 指定一个 Identity。
"Resource":制订了连接到的资源。参阅 [资源属性]。发送者 可能 指定一个资源。
实现可能定义其它的元数据属性,比如实现名、平台名等。为了互操作性,所有的元数据和语义也需要被定义为 RFCs。
以 "X-" 开头的元数据属性被保留给应用使用。
套接字类型属性
Socket-Type 属性宣告了 ZMQ 发送端的套接字类型。Socket-Type 需要遵循下面的语法:
socket-type = "REQ" | "REP"
| "DEALER" | "ROUTER"
| "PUB" | "XPUB"
| "SUB" | "XSUB"
| "PUSH" | "PULL"
| "PAIR"
| "CLIENT" | "SERVER"
| "RADIO" | "DISH"
| "SCATTER" | "GATHER"
| "PEER"
| "CHANNEL"
对等点强制要求对方使用有效的套接字类型,对于每种套接字,有效的对等套接字如下:
REQ: REP, ROUTER
REP: REQ, DEALER
DEALER: REP, DEALER, ROUTER
ROUTER: REQ, DEALER, ROUTER
PUB: SUB, XSUB
XPUB: SUB, XSUB
SUB: PUB, XPUB
XSUB: PUB, XPUB
PUSH: PULL PULL: PUSH
PAIR: PAIR
CLIENT: SERVER
SERVER: CLIENT
RADIO: DISH
DISH: RADIO
SCATTER: GATHER
GATHER: SCATTER
PEER: PEER
CHANNEL: CHANNEL
如果对等点不是一个有效的套接字,则需要返回要给 ERROR 命令,并关闭连接。
Identity 属性
资源属性
套接字语义
发布订阅模式
实现应当遵循 [./pubsub] 中规定的 PUB/XPUB, SUB/XSUB 中的语义。
使用 ZMTP 的时候,过滤可能发送在发送端(PUB/XPUB 套接字)。要创建一个订阅,SUB/XSUB 需要发送 SUBSCRIBE 命令,其语法如下:
subscribe = command-size %d9 "SUBSCRIBE" subscription
subscription = *OCTET
要取消订阅,SUB/XSUB 需要发送 CANCEL 命令:
cancel = command-size %d6 "CANCEL" subscription
subscription 是一个二进制字符串,匹配所有订阅者想要的消息。例如订阅 'A' 匹配所有以 'A' 开头的消息,空的订阅匹配所有的消息。
订阅是累加的而不是幂等的。也就是说两个相同的订阅 'A' 需要两次取消才能撤销。
心跳
ZMTP/3.1 提供连接检测信号以解决一些特定问题:
网络连接可能因超时断开,而不会报告 TCP 错误。通过检测缺少传入流量,对等方可以推断出连接已断开。
进程可能会被阻塞,尤其是当内存不足时。TCP 不会报告错误,但对于断开的连接,可以使用流量不足来推断致命错误。
要调用单个检测信号,对等方可以在安全握手完成后的任何时候发送 PING 命令:
ping = command-size %d4 "PING" ping-ttl ping-context
ping-ttl = 2OCTET
ping-context = 0*16OCTET
ping-ttl 是网络字节序的 16 位无符号整数,可以为零,也可以包含以十分之一秒为单位的生存时间。ping-ttl 要求对等方在此时间内没有收到进一步流量,则断开连接, 因此,最大 TTL 为 6553.5 秒。
当对等体收到 PING 命令时,它应使用 ping-context 的 PONG 命令进行响应,该命令可以是空的,并且不得超过 16 个八位字节:
pong = command-size %d4 "PONG" ping-context
ping-context = 0*16OCTET
当对等方在合理的时间间隔后没有收到回复时,它可能会认为连接已断开,并关闭它。应选择适合相关应用程序用例的时间间隔。此超时间隔通常是 PING 间隔的一小倍。
由于 PONG 回复可能会被任意延迟到已经排队的流量之后,因此对等方应将任何传入流量(而不仅仅是 PONG 回复)视为存活的标志。
为避免 PONG 风暴,对等体不应在未收到 PONG 应答的情况下发送大量的 PING 命令。
在以下情况下,对等方应将连接视为连接断开:
如果它发送 PING 命令,并且在某个超时内未收到任何流量。
如果它收到具有非零 TTL 的 PING 命令,然后没有在该 TTL 内接收任何进一步的流量。
检测信号失败后,对等方应正常重新连接。在此类事件发生后没有特定的恢复策略。
向后互操作性
工作的例子
假设 DEALER 连接了 ROUTER 服务器。两者运行的 ZMTP 都实现了向后兼容性,对等端将会使用 NULL 安全机制来通信。
客户端发送部分 greeting (11B) 到服务器,同时(在收到任意客户端的请求之前),服务器也会发送部分 greeting:
客户端和服务器读取主版本号(%x03)并发送剩余的 greeting:
客户端和服务器现在使用 NULL 安全机制握手。搜先客户端发送 READY 命令用来指令 DEALER Socket-Type 和空的 Identity 属性。
服务器验证套接字类型并接受,然后回复 READY 命令。(只包含了 Socket-Type 属性)(ROUTER 套接字不发送 identity):
一旦 server 发送了 READY 命令,就可以发送消息给客户端。只要客户端收到了 READY 命令,就能够发送消息给服务器。