ZeroMQ: 深入研究协议





5.00/5 (9投票s)
使用 Wireshark 检查 ZeroMQ 套接字之间交换的数据包;并通过一个简单的 PUSH-PULL 示例,研究 ZMTP 协议及其用于管理这些数据包的语法。
我关于 ZeroMQ 的文章
引言
ZeroMQ 定义了一个名为 ZMTP (ZeroMQ Message Transport Protocol) 的协议,作为在 TCP 等已连接传输层之间交换消息的传输层协议(参见 RFCs:23/ZMTP 和 37/ZMTP)。
协议(Protocol)是一组规则(源自希腊语 protocollon,意为粘贴在手稿首页用于描述其内容的第一个叶子),用于管理两个端点之间的数据交换。每个协议都有其自身的数据格式、发送时机、接收后如何管理等规则。
在本文中,我们将通过一个简单的 ZeroMQ Push-Pull 示例来解释 ZMTP 协议。为了捕获两个套接字之间交换的数据,我们将使用 Wireshark。
Wireshark
Wireshark 是一款免费开源的网络协议分析器。它能够捕获和解码网络数据包。其预期用途之一是学习网络协议的内部细节。
Wireshark 的解码过程使用 dissector(解析器),这些解析器可以识别协议字段并将其值以人类可读的格式显示在帧中。它支持数千种 dissector,用于解析和解码常用协议。
下面的 Wireshark 屏幕截图显示了 ZeroMQ Push 和 Pull 套接字之间交换的捕获数据包。
主窗口由三个窗格组成:
- 数据包列表窗格:显示捕获数据包的摘要。通过单击此窗格中的数据包,其他两个窗格的内容会随之改变。
- 数据包详情窗格:显示数据包列表窗格中选定数据包的协议和协议字段。
- 数据包字节窗格:以十六进制和 ASCII 格式显示选定数据包的内容,并高亮显示在详情窗格中选定的字段。
在上面的 Wireshark 屏幕截图中,协议层次结构为 Ethernet-Internet Protocol-Transmission Control Protocol。在每个协议级别,协议 dissector 解码其协议部分,并将数据传递给最低级别的 dissector。
Wireshark 的一个关键优势在于能够添加新的自定义 dissector,无论是作为插件还是直接集成到源代码中。
我们注意到,在 TCP 级别显示我们的套接字消息数据时,它没有被解码,因为 ZMTP dissector 尚未安装。 ZMTP dissector (zmtp-wireshark) 是一个用 Lua 编写的 Wireshark 插件,支持 ZMTP 3.0 及更高版本。
安装此 dissector 后,上面显示的 Push 和 Pull 套接字之间的相同通信,这次将带有 ZMTP dissector。
我们注意到“数据包列表”窗格中的数据包已被解码,并在列中显示了 ZMTP 信息。我们还注意到,此 dissector 是如何解码 ZMTP 数据并在“数据包详情”窗格中以可读且优雅的格式显示其字段的。
我们将在以下各节中解释每个 ZMTP 元素。
PUSH-PULL 模式示例
在此示例中,我们有一个 push 套接字连接到一个 pull 套接字。
这是代码
#include "zhelpers.hpp"
int main() {
// Create context
zmq::context_t context(1);
// Create server socket
zmq::socket_t server (context, ZMQ_PUSH);
server.bind("tcp://*:5557");
// Create client socket
zmq::socket_t client (context, ZMQ_PULL);
client.connect("tcp://:5557");
// Send message from server to client
s_send (server, "My Message");
std::string msg = s_recv (client);
std::cout << "Received: " << msg << std::endl;
server.close();
client.close();
context.close();
}
Push
套接字将向 Pull
套接字发送一条消息。
捕获数据包
启动 Wireshark 并添加 'zmtp
' 过滤器以仅过滤 zmpt
数据包,然后运行 Push-Pull 示例。捕获的数据包显示在以下屏幕截图中。
ZMTP dissector 已识别出其数据包,并以可读格式在“数据包列表”和“数据包详情”窗格中进行了解码和显示。
在两个套接字之间交换的 ZMTP 数据包称为“连接”。一个“连接”由三组数据包组成:问候、握手和流量,如上屏幕截图所示。
定义 ZMTP 的 ABNF 语法为:
zmtp = *connection
connection = greeting handshake traffic
现在,让我们检查每个数据包。
问候
问候语由 64 个字节组成,包含对等方发送的数据,用于就连接的版本和安全机制达成一致。
问候语包括签名、版本、机制、作为服务器(as-server)和填充(filler)。定义问候语的 ABNF 语法为:
greeting = signature version mechanism as-server filler
dissector 显示的每个问候语(在此示例中)由两个套接字之间交换的三个数据包组成。要查看这些数据包:
- 在(“数据包列表”窗格中)任意数据包上单击鼠标右键,然后从上下文菜单中选择“跟随 TCP 流”。
- 选择“十六进制转储”单选按钮。
在“跟随 TCP 流”对话框中,数据包数据用蓝色和红色着色。蓝色表示从 Push 到 Pull 套接字的数据包,而从 Pull 到 Push 套接字的数据包则标有红色。
签名和版本
ZMTP 签名是 10 个字节,后面跟着 2 个字节的 ZMTP 版本。定义签名和版本的 ABNF 语法为:
signature = %xFF padding %x7F
padding = 8OCTET
version = version-major version-minor
version-major = %x03
version-minor = %x00
ZMTP 签名和版本是问候语的一部分,使对等方能够检测和处理协议的旧版本,这意味着对等方可以降级其协议以与较低协议的对等方通信。但是,如果对等方无法将其协议降级以匹配其对等方,则将关闭连接。
在我们的示例中,两个对等方使用的 ZMTP 版本是 3.0(主版本 = 3,次版本 = 0)。
请注意,签名中的填充字段可能用于旧协议检测。
机制
安全机制是 ASCII 字符串,根据需要用 null 填充,以符合 20 个字节。定义安全机制的 ABNF 语法为:
mechanism = 20mechanism-char
mechanism-char = "A"-"Z" | DIGIT | "-" | "_" | "." | "+" | %x0
安全机制确保对等方知道与之通信的另一方的身份,这样消息就不会被第三方篡改或检查。安全机制还定义了握手阶段,该阶段由问候语之后对等方之间交换的一些数据包组成。如果一方收到的安全机制与其发送的安全机制不完全匹配,则将关闭连接。
在我们的示例中,套接字定义了 no security mechanism“NULL
”,这意味着没有身份验证也没有机密性。
“NULL
”安全机制定义了在对等方之间交换的一个命令,该命令构成了我们将在本文稍后讨论的握手阶段。
作为服务器 (AS-Server)
'as-server
' 由一个字节组成。定义 as-server 的 ABNF 语法为:
as-server = %x00 | %x01
“as-server
”指示对等方是作为服务器(值为 1)还是作为客户端(值为 0)运行。这些值由安全机制定义,与套接字绑定/连接方向无关(例如,在“PLAIN
”安全机制中,被定义为客户端的对等方通过发送 HELLO
命令向被定义为服务器的对等方进行身份验证。服务器接受或拒绝此身份验证)。
NULL
安全机制不指定客户端和服务器拓扑,因此所有对等方的“as-server”字段都应始终为零。
填充 (Filler)
“filler
”用零将问候语扩展到 64 个字节,其语法为:
filler = 31%x00
握手
帧化数据
问候之后,所有数据都以帧的形式发送。一个帧由一个标志字段(1 字节)组成,后跟一个大小字段(一个字节或八个字节),以及一个大小为 size 的帧主体。大小不包括标志字段本身,因此空帧的大小为零。
标志由一个字节组成,包含各种控制标志。
-
位 0 (MORE)
-
0:表示没有后续帧。
-
1:表示有另一个帧要跟随。
-
-
位 1 (LONG)
-
0:表示帧大小以网络字节序编码为 64 位无符号整数。
-
1:表示帧大小编码为单个字节。
-
-
位 2 (COMMAND)
-
0:表示该帧是消息帧。
-
1:表示该帧是命令帧。
-
-
位 3-7:保留供将来使用,必须为零。
以下各节将讨论帧的示例。
握手由安全机制在问候中定义的零个或多个命令组成。命令是单个长帧或短帧。定义任何命令的 ABNF 语法为:
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 = OCTET 1*255command-name-char
command-name-char = ALPHA
command-data = *OCTET
握手是允许对等方创建安全连接的扩展协议。如果安全握手成功,对等方将继续通信,否则一个或两个对等方将关闭连接。
我们可以看到,上述语法中的“command-size
”规则要么以 0x04 开头,要么以 0x06 开头,这代表了帧的标志字段。标志 0x04 仅设置了 Bit2 为 1,表示该帧是单个短命令帧。标志 0x06 设置了 Bit1 和 Bit2 为 1,表示该帧是单个长帧。
NULL
安全机制定义了对等方之间交换的 READY
命令。 READY
命令包含一个属性列表。每个属性由一个名称-值对组成。
定义 NULL
安全机制的 ABNF 语法为:
null = ready *message | error
ready = command-size %d5 "READY" metadata
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
metadata = *property
property = name value
name = OCTET 1*255name-char
name-char = ALPHA | DIGIT | "-" | "_" | "." | "+"
value = 4OCTET *OCTET ; Size in network byte order
我们示例中的 READY
命令包含一个名为“Socket-Type
”的属性,该属性定义了发送方的套接字类型。当 push 套接字发送此命令时,此属性的值为“PUSH
”,当 pull 套接字发送时,值为“PULL
”。
对等方会验证另一方是否正在使用有效的套接字类型(套接字的有效组合)。在我们的示例中,Push 对等方会验证另一方是否具有 Pull 套接字类型,反之亦然。如果验证不成功,则连接将被关闭。
Traffic
流量由命令和消息混合组成。
定义流量的 ABNF 语法为:
traffic = *(command | message)
“command
”的语法已在上面定义,这里是消息的 ABNF 语法:
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
short-size = OCTET ; Body is 0 to 255 octets
long-size = 8OCTET ; Body is 0 to 2^63-1 octets
message-body= *OCTET
消息帧的 flags
字节在“message-more
”和“message-last
”规则中定义。此字段可以取四个值:
0x00: indicates that this frames is a short last-frame message
0x02: indicates that this frames is a long last-frame message
0x01: indicates that this frames is a short more-frame message
0x03: indicates that this frames is a long more-frame message
在我们的示例中,流量由一条消息组成。
现在,我们将发送一条多帧消息。第一帧是长消息(长度 > 255 字节),第二帧是短消息(与我们之前发送的相同)。
s_sendmore (server, std::string(256, 'a'));
s_send (server, "My Message");
在上图截图中,我没有在“Packet Bytes
”窗格中显示所有长消息字节。我们注意到第一帧的标志表明该帧后面还有另一帧(More),并且它是一个长帧(位 1 设置为 1)。有效载荷长度被编码为 64 位无符号整数(8 字节),因为它大于 255 字节。
第二帧紧跟在第一帧之后。其标志表明它是最后一帧(位 0 设置为 0),并且它是一个短帧(位 1 设置为 0),因为有效载荷长度小于 256 字节。
结论
ZMTP 是一个管理 ZeroMQ 套接字之间数据交换的协议。它定义了若干规则:协议版本、安全机制、定义离散消息(帧)、元数据(单/多帧、短/长消息)等。