37/ZMTP
ZeroMQ 消息传输协议
- 状态:草稿
- 编辑:Pieter Hintjens ph@imatix.com
ZeroMQ 消息传输协议 (ZMTP) 是一个传输层协议,用于在连接的传输层(如 TCP)之上在两个对等方之间交换消息。本文档描述了 ZMTP 3.1。该版本增加了 JOIN、LEAVE、SUBSCRIBE、CANCEL、PING 和 PONG 命令以及端点资源。
前言
版权所有 (c) 2009-2015 iMatix Corporation 及贡献者
本规范是自由软件;您可以根据自由软件基金会发布的 GNU 通用公共许可证条款重新分发和/或修改它;可以是该许可证的第 3 版,或(由您选择)任何后续版本。分发本规范是希望它有用,但不提供任何担保;甚至不包括适销性或特定用途适用性的默示担保。更多详情请参见 GNU 通用公共许可证。您应已随本程序收到一份 GNU 通用公共许可证副本;如果没有,请参阅 https://gnu.ac.cn/licenses。
本规范是一个 自由开放标准,受数字标准组织(Digital Standards Organization)的 共识导向规范系统 管辖。
本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按 RFC 2119 中的描述进行解释。
目标
ZeroMQ 消息传输协议 (ZMTP) 是一个传输层协议,用于在连接的传输层(如 TCP)之上在两个对等方之间交换消息。本文档描述了 ZMTP 3.1 版本。ZMTP 解决了在使用 TCP 传输消息时遇到的一些问题:
-
TCP 传输的是无分隔符的八位字节流,但我们希望发送和接收离散的消息。因此,ZMTP 读写由大小和主体组成的帧。
-
我们需要在每个帧上携带元数据(例如,帧是否是多部分消息的一部分)。ZMTP 在每个帧中提供了一个用于元数据的标志字段。
-
我们需要能够与旧实现通信,以便我们的帧结构可以演进而不破坏现有实现。ZMTP 定义了一个握手信息(greeting),用于宣布实现的版本号,并指定了版本协商的方法。
-
我们需要安全性,以便对等方能够确定与之通信的对等方的身份,并且消息不会被第三方篡改或查看。ZMTP 定义了一个安全握手,允许对等方建立安全连接。
-
我们需要一系列安全协议,从明文(不安全,但快速)到完全认证和加密(安全,但慢)。此外,随着时间的推移,我们需要自由地添加新的安全协议。ZMTP 定义了一种方法,允许对等方协商可扩展的安全机制。
-
我们需要一种方式在安全握手后携带关于连接的元数据。ZMTP 定义了一套标准的元数据属性(socket 类型、身份等),供对等方在安全机制协商后交换。
-
我们希望允许多个任务共享一个外部唯一接口和端口,以降低系统管理成本。
-
我们需要以一种易于团队在任何平台和任何语言上实现的方式记录这些解决方案。因此,ZMTP 被指定为一个形式化协议(即本文档),并在自由许可下提供给各团队。
-
我们需要保证人们不会创建 ZMTP 的私有分支,从而破坏互操作性。因此,ZMTP 在 GPLv3 下获得许可,以确保任何派生版本也必须提供给实现该协议的软件的用户。
与 ZMTP 3.0 的变化
ZMTP 定义了一个资源元数据属性,允许任意数量的任务共享一个接口和端口。
相关规范
- https://rfc.zeromq.cn/spec:23/ZMTP 定义了 ZMTP v3.0。
- https://rfc.zeromq.cn/spec:24/ZMTP-PLAIN 定义了 ZMTP-PLAIN 安全机制。
- https://rfc.zeromq.cn/spec:25/ZMTP-CURVE 定义了 ZMTP-CURVE 安全机制。
- https://rfc.zeromq.cn/spec:26/CURVEZMQ 定义了 CurveZMQ 认证和加密协议。
- https://rfc.zeromq.cn/spec:27/ZAP 定义了 ZeroMQ 认证协议。
- https://rfc.zeromq.cn/spec:28/REQREP 定义了 REQ、REP、DEALER 和 ROUTER socket 的语义。
- https://rfc.zeromq.cn/spec:29/PUBSUB 定义了 PUB、XPUB、SUB 和 XSUB socket 的语义。
- https://rfc.zeromq.cn/spec:30/PIPELINE 定义了 PUSH 和 PULL socket 的语义。
- https://rfc.zeromq.cn/spec:31/EXPAIR 定义了独占 PAIR socket 的语义。
- https://rfc.zeromq.cn/spec:41/CLIENTSERVER 定义了 CLIENT 和 SERVER 的语义。
- https://rfc.zeromq.cn/spec:48/RADIODISH 定义了 RADIO 和 DISH 的语义。
- https://rfc.zeromq.cn/spec:49/SCATTERGATHER 定义了 SCATTER 和 GATHER 的语义。
- https://rfc.zeromq.cn/spec:51/P2P 定义了 PEER 的语义。
- https://rfc.zeromq.cn/spec:52/CHANNEL 定义了 CHANNEL 的语义。
实现
总体行为
ZMTP 连接经历以下主要阶段:
-
两个对等方通过相互发送数据来协商连接的版本和安全机制,然后决定是继续协商还是关闭连接。
-
两个对等方通过交换零个或多个命令来握手安全机制。如果安全握手成功,对等方继续协商;否则,一个或两个对等方关闭连接。
-
然后,每个对等方将关于连接的元数据作为最终命令发送给对方。对等方可以检查元数据,每个对等方决定是继续还是关闭连接。
-
然后,每个对等方都可以向对方发送消息。任一对等方可以在任何时候关闭连接。
形式化语法
以下 ABNF 语法定义了该协议:
; The protocol consists of zero or more connections
zmtp = *connection
; A connection is a greeting, a handshake, and traffic
connection = greeting handshake traffic
; The greeting announces the protocol details
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
; The mechanism is a null padded string
mechanism = 20mechanism-char
mechanism-char = "A"-"Z" | DIGIT
| "-" | "_" | "." | "+" | %x0
; Is the peer acting as server for security handshake?
as-server = %x00 | %x01
; The filler extends the greeting to 64 octets
filler = 31%x00 ; 31 zero octets
; The handshake consists of at least one command
; The actual grammar depends on the security mechanism
handshake = 1*command
; Traffic consists of commands and messages intermixed
traffic = *(command | message)
; A command is a single long or short frame
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
; A message is one or more frames
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 字符串,必要时以空字符填充以适应 20 个八位字节。实现可以定义自己的机制用于实验和内部使用。所有旨在公共互操作性的机制应定义为 0MQ RFC。机制名称应按先到先得的原则分配。机制名称应仅由大写字母 A 到 Z、数字以及嵌入的连字符或下划线组成。
与允许服务器宣布多种安全机制的 SASL 不同,对等方精确地宣布一种安全机制。ZMTP 的安全性是断言式的(assertive),即给定 socket 上的所有对等方都具有相同的、必需的安全级别。这可以防止降级攻击并简化实现。
每种安全机制定义了一个协议,该协议由任一对等方向对方发送的零个或多个命令组成,直到握手完成,或者任一对等方拒绝继续并关闭连接。
命令是单帧,由一个标志八位字节、一个大小字段和一个主体组成。如果主体大小为 0-255 个八位字节,命令应使用短大小字段(%x04 后跟 1 个八位字节)。如果主体大小为 256 个或更多八位字节,命令应使用长大小字段(%x06 后跟 8 个八位字节)。
标志八位字节和大小字段始终是明文。主体可以部分或完全加密。ZMTP 不定义命令的语法和语义。这些完全由安全机制协议定义。
支持的机制不被视为敏感信息。读取了完整握手信息(包括机制)的对等方,也必须发送包含机制的完整握手信息。这避免了两个对等方相互等待对方透露其余握手信息而导致的死锁。
如果对等方收到的机制与其发送的机制不完全匹配,则必须关闭连接。
错误处理
ZMTP 允许在机制握手期间使用 ERROR 命令显式表示致命错误响应。对等方应将接收到的 ERROR 命令视为致命错误,并通过关闭连接来处理,并且不应使用相同的安全凭据重新连接。
实现应通过关闭连接来指示任何其他错误,例如过载、临时拒绝连接等。对等方应将意外的连接关闭视为临时错误,并应重新连接。
为避免连接风暴,对等方应在短时间(可能是随机间隔)后重新连接。此外,如果对等方重新连接多次,应增加重连之间的延迟。有多种策略可用。
帧结构
握手信息(大小固定为 64 个八位字节)之后,所有后续数据都以帧的形式发送,帧携带命令或消息。帧的设计旨在对小帧高效,同时也能处理非常大的数据。
一个帧由一个标志字段(1 个八位字节)、一个大小字段(1 个或 8 个八位字节)和一个大小为 size 个八位字节的帧主体组成。该大小不包括标志字段或其自身的大小,因此空帧的大小为零。
短帧的主体大小为 0 到 255 个八位字节。长帧的主体大小为 0 到 2^63-1 个八位字节。
标志字段由一个包含各种控制标志的八位字节组成。位 0 是最低有效位(最右边的位)。
-
位 7-3:保留。位 7-3 保留供将来使用,并且必须为零。
-
位 2 (COMMAND):命令帧。值为 1 表示该帧是命令帧。值为 0 表示该帧是消息帧。
-
位 1 (LONG):长帧。值为 0 表示帧大小编码为一个八位字节。值为 1 表示帧大小编码为一个 64 位无符号整数,采用网络字节序。
-
位 0 (MORE):后续还有帧。值为 0 表示没有后续帧。值为 1 表示后续还有更多帧。在命令帧中,此位应为零。
命令
命令由 ZMTP 实现使用,通常应用程序不可见,但某些情况除外。命令始终由一个帧组成,包含一个可打印的命令名称、一个空八位字节分隔符和数据。
本规范定义了以下命令:
- READY 和 ERROR - 实现 NULL 安全握手,详见下面的“NULL 安全机制”。
- SUBSCRIBE 和 CANCEL - 订阅管理,详见下面的“Publish-Subscribe 模式”。
- PING 和 PONG - 心跳请求和回复,详见下面的“连接心跳”。
ZMTP 支持可扩展的安全机制,这些机制可以定义自己的命令。安全机制可以使用所需的任何命令名称。
消息
消息携带应用程序数据,通常不由 ZMTP 实现创建、修改或过滤,但某些情况除外。消息由一个或多个帧组成,实现应始终原子地发送和传递消息,即发送消息的所有帧,或者不发送任何帧。
NULL 安全机制
NULL 机制不实现认证和保密性。在没有传输层安全(例如通过 VPN)的公共基础设施上,不应使用 NULL 机制。
当对等方使用 NULL 安全机制时,as-server 字段必须为零。绑定的对等方应是服务器,连接的对等方应是客户端。
要完成 NULL 安全握手,客户端应发送一个 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
消息和命令大小由前面解释的 ZMTP 语法定义。
READY 命令的主体包含一个属性列表,属性由大小指定的字符串形式的名称和值组成。
名称应为 1 到 255 个字符。大小为零的名称无效。名称的大小写(大写或小写)不应具有重要性。
值应为 0 到 2,147,483,647 (2^31-1 或 C/C++ 中的 INT32_MAX) 个八位字节的不透明二进制数据。允许大小为零的值。值的语义取决于属性。值大小字段应为四个八位字节,采用网络字节序。请注意,这个大小字段在内存中大部分情况下不会对齐。
ERROR 命令的主体包含可供记录的错误原因。它没有定义的语义值。
连接元数据
安全机制应提供对等方以键值字典形式交换元数据的方法。元数据的具体编码取决于机制。
元数据名称应不区分大小写。
定义了以下元数据属性:
-
“Socket-Type”,指定发送方的 socket 类型。详见下面的“Socket 类型属性”部分。发送方应指定 Socket-Type。
-
“Identity”,指定发送方的 socket 身份。详见下面的“身份属性”部分。发送方可以指定 Identity。
-
“Resource”,指定要连接的资源。详见下面的“资源属性”部分。发送方可以指定 Resource。
实现可以提供其他元数据属性,例如实现名称、平台名称等。为了互操作性,元数据名称和语义可以定义为 RFC。
以“X-”开头的元数据名称应保留供应用程序使用。
Socket 类型属性
Socket-Type 宣布了发送方对等方的 ZeroMQ socket 类型。Socket-Type 应匹配以下语法:
socket-type = "REQ" | "REP"
| "DEALER" | "ROUTER"
| "PUB" | "XPUB"
| "SUB" | "XSUB"
| "PUSH" | "PULL"
| "PAIR"
| "CLIENT" | "SERVER"
| "RADIO" | "DISH"
| "SCATTER" | "GATHER"
| "PEER"
| "CHANNEL"
对等方应强制要求对方使用有效的 socket 类型。对于每种 socket 类型,其合法的对等方类型如下:
-
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
当对等方验证 socket 类型时,应通过返回 ERROR 命令并随后断开与对等方的连接来处理错误。
身份属性
连接到 ROUTER 的 REQ、DEALER 或 ROUTER 对等方可以宣布其身份,该身份被 ROUTER socket 用作寻址机制。对于所有其他 socket 类型,Identity 属性应被忽略。
Identity 应匹配以下语法:
identity = 0*255OCTET
Identity 的第一个八位字节不应为零:以零八位字节开头的身份保留用于实现的内部使用。
NEW: 资源属性
资源属性解决了在单个网络端点上运行多个服务(在单个进程内)的常见问题。当使用面向公共的端口时尤其需要,由于防火墙问题,管理成本可能很高。如果没有协议支持,一个服务需要绑定到一个端口号。有了协议支持,多个服务可以共享一个端口。
ZMTP 使用“资源”概念实现这种共享。
zmq_bind (server_socket, "tcp://eth0:6000/system/name-service/test");
资源由终端用户应用程序选择,并在概念上扩展了 zmq_bind 或 zmq_connect API 调用。
zmq_connect (client_socket, "tcp://192.168.55.212:6000/system/name-service/test");
例如:
以及:
resource = resource-name * ( "/" resource-name )
resource-name = 1*resource-char
resource-char = ALPHA | DIGIT | "$" | "-" | "_" | "@" | "." | "&" | "+"
其中资源即为“system/name-service/test”。
Resource 应匹配以下语法:
- 实现应接受所有资源请求,因为资源可能在连接建立后的任意时间可用。
- 由于资源评估发生在安全握手之后,所有使用同一端点的服务应使用相同的安全凭据。实现这一目标的一种策略是:
- 为首次绑定到特定接口:端口创建监听器。
- 按监听器存储安全凭据。
Socket 语义
按监听器处理新的传入连接。
在安全握手后解析资源名称,并将连接移交给适当的服务。
-
为了确保互操作性,我们旨在定义 socket 特定的 API 语义(供调用应用程序使用)以及网络上对等方之间的通信行为。
-
以下规则适用于所有 socket:
-
所有 socket 应接受连接(绑定到地址)并建立连接。
-
所有 socket 应机会性地建立连接,即:它们异步连接到端点,如果连接断开,应在适当的延迟后重新连接。
-
消息应原子地发送或接收;即,所有帧或无帧。在发送时,对等方应将消息的所有帧排队在内存中,直到发送最后一个帧。
Request-Reply 模式
消息不应多次传递给任何对等方。
Publish-Subscribe 模式
两个直接对等方之间的所有消息应按顺序传递。
实现应遵循 https://rfc.zeromq.cn/spec:28/REQREP 中关于 REQ, REP, DEALER 和 ROUTER socket 语义的规定。
subscribe = command-size %d9 "SUBSCRIBE" subscription
subscription = *OCTET
实现应遵循 https://rfc.zeromq.cn/spec:29/PUBSUB 中关于 PUB, XPUB, SUB 和 XSUB socket 语义的规定。
cancel = command-size %d6 "CANCEL" subscription
使用 ZMTP 时,消息过滤应发生在发布方(PUB 或 XPUB socket)。要创建订阅,SUB 或 XSUB 对等方应发送一个 SUBSCRIBE 命令,其语法如下:
要取消订阅,SUB 或 XSUB 对等方应发送一个 CANCEL 命令,其语法如下:
Pipeline 模式
订阅是一个二进制字符串,指定订阅者想要哪些消息。订阅“A”应匹配所有以“A”开头的消息。空订阅应匹配所有消息。
独占 Pair 模式
订阅应是累加的,且不应是幂等的。也就是说,订阅“A”和“”与仅订阅“”相同。订阅“A”和“A”算作两个订阅,需要两次 CANCEL 命令才能撤销。
Client-Server 模式
实现应遵循 https://rfc.zeromq.cn/spec:30/PIPELINE 中关于 PUSH 和 PULL socket 语义的规定。
Radio-Dish 模式
实现应遵循 https://rfc.zeromq.cn/spec:31/EXPAIR 中关于独占 PAIR socket 语义的规定。
实现应遵循 https://rfc.zeromq.cn/spec:47/CLIENTSERVER 中关于 CLIENT 和 SERVER socket 语义的规定。
join = command-size %d4 "JOIN" group
group = 0*255group-char
group-char = %d1-255
实现应遵循 https://rfc.zeromq.cn/spec:48/RADIODISH 中关于 RADIO 和 DISH socket 语义的规定。
leave = command-size %d5 "LEAVE" group
使用 ZMTP 时,组员资格检查应发生在 RADIO 端。要加入一个组,DISH 对等方应发送一个 JOIN 命令,其语法如下:
要离开一个组,DISH 对等方应发送一个 LEAVE 命令,其语法如下:
radio-dish-message = group-part body-part
group-part = %x01 short-size group
body-part = message-last
group = 0*255group-char
group-char = %d1-255
Scatter-Gather 模式
组是一个字符串,消息被发送到一个组,组的所有成员将收到消息。组匹配是精确的。
Peer-to-Peer 模式
使用 ZMTP 时,RADIO 通过网络发送组和消息,作为一个两部分消息,其语法如下:
CHANNEL 模式
实现应遵循 https://rfc.zeromq.cn/spec:49/SCATTERGATHER 中关于 SCATTER 和 GATHER socket 语义的规定。
实现应遵循 https://rfc.zeromq.cn/spec:51/P2P 中关于 PEER socket 语义的规定。
实现应遵循 https://rfc.zeromq.cn/spec:52/CHANNEL 中关于 CHANNEL socket 语义的规定。
NEW: 线程安全 Socket 类型
线程安全是一类新的 socket 类型系列,它们可以安全地从多个线程中使用,无论用于发送还是接收。
为了线程安全,socket API 必须是原子的,send 调用必须发送整个消息,receive 调用必须接收整个消息。因此,线程安全 socket 不允许多部分消息。
线程安全 socket 必须禁止发送多部分消息,并且必须丢弃从网络上接收到的任何多部分消息。
- 任何附加元数据(例如 routing-id 或 group)必须附加到消息上。
- 属于线程安全系列的 socket 类型:
- CLIENT
- SERVER
- RADIO
- DISH
- SCATTER
- GATHER
PEER
CHANNEL
连接心跳
多部分消息定义
-
多部分消息是多个连续的 ZMTP 消息,其中除最后一个消息外,所有消息的 MORE 标志都已设置。
-
ZMTP/3.1 提供了连接心跳机制,以解决一些特定问题:
网络连接可能变旧并死亡,而不会报告 TCP 错误。通过检测传入流量的缺失,对等方可以推断连接已变旧。
ping = command-size %d4 "PING" ping-ttl ping-context
ping-ttl = 2OCTET
ping-context = 0*16OCTET
进程可能会阻塞,特别是如果内存不足时。TCP 不会报告错误,但就像旧连接一样,流量的缺失可用于推断致命错误。
要发起一次心跳,对等方可以在安全握手完成后的任何时候发送一个 PING 命令:
pong = command-size %d4 "PONG" ping-context
ping-context = 0*16OCTET
ping-ttl 是一个采用网络字节序的 16 位无符号整数,可以为零,也可以包含以十分之一秒为单位测量的时间生存期(time-to-live)。ping-ttl 为对方对等方提供了一个强烈的提示,如果在该时间后未收到进一步流量,则断开连接。因此,最大 TTL 为 6553.5 秒。
当对等方收到 PING 命令时,应回复一个 PONG 命令,该命令回显 ping-context,ping-context 可以为空,并且绝不能超过 16 个八位字节。
当对等方在合理间隔后未收到回复时,它可以认为连接已死并关闭它。间隔时间应选择适合相关的应用程序用例。这个超时间隔通常是 PING 间隔的几倍。
由于 PONG 回复可能任意延迟在已排队流量之后,对等方应将任何传入流量(不仅仅是 PONG 回复)视为连接存活的迹象。
- 为避免 PONG 风暴,对等方在未收到 PONG 回复的情况下,不应发送超过少量 PING 命令。
- 在以下情况下,对等方应认为连接已死:
如果发送了 PING 命令并在某个超时时间内未收到任何流量。
向后兼容性
如果收到带有非零 TTL 的 PING 命令,并在该 TTL 内未收到任何进一步流量。
心跳失败后,对等方应照常重新连接 - 在此类事件后没有特定的恢复策略。
检测 ZMTP 2.0 对等方
为了检测和使用旧版本的 ZMTP,我们定义了两种策略;一种只检测 2.0 对等方,另一种检测 1.0 和 2.0 对等方。
-
当对等方不需要向后兼容时,应在建立新的对等连接后立即发送其完整的握手信息。
-
从 [ZMTP 2.0] 开始,协议在签名之后立即包含版本号。为了检测 ZMTP 2.0 对等方并与之互操作,实现可以使用此策略:
-
发送 10 个八位字节的签名,后跟主要版本号(单个八位字节 %x03)。
-
等待对方对等方发送其握手信息。
如果对等方版本号为 1 或 2,则该对等方正在使用 ZMTP 2.0,因此发送 ZMTP 2.0 socket 类型和身份,并继续使用 ZMTP 2.0。
C:signature + major-version S:signature + major-version
C:rest of greeting S:rest of greeting
C:ready S:ready
C:message... S:message...
检测 ZMTP 1.0 和 2.0 对等方
如果对等方版本号为 3 或更高,则该对等方正在使用 ZMTP 3.x,因此发送其余的握手信息,并继续使用 ZMTP 3.1 或 3.0。
-
以下是展示两个 ZMTP 3.1 对等方使用此算法的命令序列:
-
ZMTP 1.0 没有版本信息。为了检测 ZMTP 1.0 和 2.0 对等方并与之互操作,实现可以使用此策略:
-
发送 10 个八位字节的伪签名,包含“%xFF size %x7F”,其中“size”是发送方身份的八位字节数(0 或更大)加 1。size 应为 8 个八位字节,采用网络字节序,并占用填充字段。
-
从对方对等方读取第一个八位字节。
-
如果第一个八位字节不是 %FF,则对方对等方正在使用 ZMTP 1.0,并发送了其身份的短帧长度。我们读取相应数量的八位字节。
-
如果第一个八位字节是 %FF,则我们读取另外九个八位字节,并检查最后一个八位字节(总共第 10 个)。如果最低有效位为 0,则对方对等方正在使用 ZMTP 1.0,并发送了其身份的长长度。我们读取相应数量的八位字节。
-
如果最低有效位为 1,则该对等方正在使用 ZMTP 2.0 或更高版本,并发送了 ZMTP 签名。我们再读取一个八位字节,它指示 ZMTP 版本。如果此版本为 1 或 2,则我们有一个 ZMTP 2.0 对等方。如果此版本为 3,则我们有一个 ZMTP 3.x 对等方。
-
如果实现的安全性机制不是 NULL,并且检测到 ZMTP 1.0 或 2.0 对等方,则必须立即关闭连接。ZMTP 1.0 或 2.0 对等方只能请求 NULL 安全性。
-
当我们检测到 ZMTP 1.0 对等方时,我们已经发送了 10 个八位字节,对方对等方将其解释为身份帧的开始。我们继续发送身份帧的主体(零个或多个八位字节)。从那时起,我们使用 ZMTP 1.0 帧语法对该连接上的所有帧进行编码和解码。
示例
当我们检测到 ZMTP 2.0 对等方时,我们继续按照上述说明进行版本协商,发送我们的版本号,然后发送 ZMTP 2.0 规范定义的 socket 类型和身份。
- 当我们检测到 ZMTP 3.x 对等方时,我们继续发送握手信息的其余部分(八位字节 10-64),然后照常进行安全握手。
signature major
+------+-------------+------+------+
| %xFF | %x00...%x00 | %x7F | %x03 |
+------+-------------+------+------+
0 1 - 8 9 10
- 一个 DEALER 客户端连接到一个 ROUTER 服务器。客户端和服务器都运行 ZMTP,并且实现支持向后兼容检测。对等方将使用 NULL 安全机制相互通信。
minor mechanism as-server filler
+------+-----------+---------+-------------+
| %x01 | "NULL" | %x00 | %x00...%x00 |
+------+-----------+---------+-------------+
11 12 - 31 32 33 - 63
- 客户端向服务器发送部分握手信息(11 个八位字节),同时(在从客户端收到任何内容之前),服务器也发送部分握手信息:
+------+----+
| %x04 | 41 |
+------+----+
0 1
flags size
+------+---+---+---+---+---+
| %x05 | R | E | A | D | Y |
+------+---+---+---+---+---+
2 3 4 5 6 7
Command name "READY"
+----+---+---+---+---+---+---+---+---+---+---+---+
| 11 | S | o | c | k | e | t | - | T | y | p | e |
+----+---+---+---+---+---+---+---+---+---+---+---+
8 9 10 11 12 13 14 15 16 17 18 19
Property name "Socket-Type"
+------+------+------+------+---+---+---+---+---+---+
| %x00 | %x00 | %x00 | %x06 | D | E | A | L | E | R |
+------+------+------+------+---+---+---+---+---+---+
20 21 22 23 24 25 26 27 28 29
Property value "DEALER"
+----+---+---+---+---+---+---+---+---+
| 8 | I | d | e | n | t | i | t | y |
+----+---+---+---+---+---+---+---+---+
30 31 32 33 34 35 36 37 38
Property name "Identity"
+------+------+------+------+
| %x00 | %x00 | %x00 | %x00 |
+------+------+------+------+
39 40 41 42
Property value ""
- 服务器验证套接字类型,接受它,并回复一个只包含 Socket-Type 属性的 READY 命令(ROUTER 套接字不发送身份)
+------+----+
| %x04 | 28 |
+------+----+
+------+---+---+---+---+---+
| %x05 | R | E | A | D | Y |
+------+---+---+---+---+---+
+----+---+---+---+---+---+---+---+---+---+---+---+
| 11 | S | o | c | k | e | t | - | T | y | p | e |
+----+---+---+---+---+---+---+---+---+---+---+---+
+------+------+------+------+---+---+---+---+---+---+
| %x00 | %x00 | %x00 | %x06 | R | O | U | T | E | R |
+------+------+------+------+---+---+---+---+---+---+
服务器一旦发送了自己的 READY 命令,它就可以向客户端发送消息。客户端一旦接收到服务器的 READY 命令,它就可以向服务器发送消息。
连接的线路分析
ZMTP 连接根据安全机制可以是明文的或加密的。在明文连接上,消息数据应当作为消息帧发送。所有明文机制都应当使用本规范定义的消息帧,以便链路分析无需了解具体的机制。
在加密连接上,消息数据应当被编码为命令,以便链路分析不可能进行。
在所有 ZMTP 连接上,命令名称应当可见,命令帧应当可打印。
安全考量
-
实现必须防范降级攻击,这类攻击可能采取的形式包括伪造 ZMTP 1.0 或 2.0 协议头部,或者要求使用比应用程序请求的安全性更低的机制。
-
攻击者可能通过重复连接尝试使对端过载。因此,对端可以记录失败的访问,并可以检测和阻止来自特定源 IP 地址的重复失败连接。
-
攻击者可能试图通过保持连接开启状态导致对端内存耗尽。因此,对端可以在安全握手完成后才为连接分配内存,并且可以限制进行中的握手数量和开销。
-
攻击者可能试图发送小型请求,这些请求会产生大型响应回传到伪造的源地址(这才是攻击的真正目标)。这被称为“放大攻击”。因此,对端可以限制来自每个源 IP 地址的进行中的握手数量。
-
攻击者可能试图从实现者的元数据中发现脆弱版本。因此,ZMTP 在安全握手之后才发送元数据。
-
攻击者可以利用消息的大小(即使不破解加密)来收集关于其含义的信息。因此,在加密连接上,消息数据应当填充到随机的最小尺寸。
-
攻击者可以利用流量的简单存在与否来收集关于对端的信息(例如“用户 X 上线了”)。因此,在加密连接上,当没有其他流量时,对端应当发送随机的垃圾数据(“噪声”)。为了完全掩盖流量,对端可以限制消息发送速率,以便噪声和真实数据之间没有可见的差异。
待讨论主题
本草案开放讨论。以下议题至少仍在讨论中:
-
如果我们将套接字类型移回问候语中,我们可以使语法更加规范化,以包含诸如 XSUB 套接字的订阅消息等内容。当套接字类型存储为元数据时,这相当困难。
-
我们是否需要策略来保护 REQ 套接字在对端死亡时免受阻塞?
-
我们是否需要策略来保护 REP 套接字免受格式错误请求的影响?
-
我们是否希望 DEALER 和 PUSH 套接字将无法投递的消息重新排队发送给其他对端?这可以提高可靠性,但会导致消息乱序,并且对于在传输中丢失或已发送给其他对端的消息不够健壮。
-
信用点(Credits),作为异步流控制的信令机制。这允许对排队数据的量进行精细控制。在这种场景下,接收对端(DEALER, PULL, REP)会发送信用点,这些信用点会被路由到它的消息消耗。最简单的用例是用于 PUSH/DEALER 到 PULL/DEALER 的轮询路由。信用点可以是八位组(字节)或消息。
-
命令或其他策略,以便在未加密的网络上进行链路分析(如果没有一些额外信息,将无法确定消息何时开始)。