ZeroMQ 笔记

Introduce

ZeroMQ, 一个开源的通用消息库。

ZeroMQ(也拼作ØMQ、0MQ 或 ZMQ)是一个高性能异步消息传递库,旨在在分布式或并发应用程序中使用。它提供了一个消息队列,但与面向消息的中间件不同,ZeroMQ系统可以在没有专用消息代理的情况下运行。

ZeroMQ支持各种传输(TCP、进程内、进程间、多播、WebSocket等等)上的通用消息传递模式(发布/订阅、请求/应答、客户端/服务器等),使进程间消息传递像线程间消息传递一样简单。这使你的代码清晰,模块化和极容易扩展。

ZeroMQ是由一个大型贡献者社区开发的。许多流行的编程语言都有第三方实现,C# 和 Java 也有 Native Ports。

ZeroMQ的哲学是从零开始的。零代表零代理(ZeroMQ是无代理的)、零延迟、零成本(它是免费的)和零管理。更普遍地说,“零”指的是渗透到项目中的极简主义文化。我们通过消除复杂性而不是暴露新功能来增加功能。

Libzmq - the low level library

Libzmq (https://github.com/zeromq/libzmq) 是大多数不同语言绑定背后的底层库。Libzmq 公开 C-API 及 C++ 的实现。

Languages

C、C++、C#、Erlang、F#、Go、Haskell、Java、Node.js、Perl、Python、Ruby、Rust 等。

更多:http://wiki.zeromq.org/bindings:_start

Java

Messages

Messages

Blob

ZeroMQ 消息是在应用程序或相同应用程序的组件之间传递的离散数据单元。从ZeroMQ本身的角度来看,消息被认为是不透明的二进制数据。

在网络上,ZeroMQ 消息是可以容纳内存的从 0 开始任意大小的 Blob。可以使用 Protocol Buffers、msgpack、JSON 或应用程序需要表达的任何东西来进行自己的序列化。选择可移植的数据表示是明智的,可以自己做出权衡的决定。

Frame

最简单的 ZeroMQ 消息由一个帧(也称为消息部分)组成。

帧是 ZeroMQ 消息的基本格式,帧是指定长度的数据块。

ZeroMQ 保证传递消息的所有部分(一个或多个),或者不传递任何部分。这允许以单个消息的形式发送或接收帧列表。

Multipart

消息(单个或多个部分)必须装入内存。如果想发送任意大小的文件,应该将它们分解为多个块(piece),并将每个块(piece)作为单个部分消息(single-part)发送。

使用 Multipart 数据不会减少内存消耗。

Working with strings

将数据作为字符串传递通常是进行通信的最简单方法,因为序列化非常简单。对于ZeroMQ,有这样的规则:字符串是指定长度的,并且在发送时不带末尾null

以下函数(JeroMQ 为例)将字符串作为单帧消息发送到套接字,其中字符串的长度等于帧的长度。

1
socket.send("HELLO");

要从套接字读取字符串,只需调用 recv 函数。

1
String hello = socket.recvStr()

如果需要更多的控制帧,可以使用 ZFrame 类。下面的代码片段展示了如何从字符串创建帧并发送帧。

1
2
ZFrame stringFrame = new ZFrame("HELLO");
stringFrame.send(socket, 0);

调用 ZFrame 类的静态 recvFrame 函数,从套接字中读取一个帧并返回一个 ZFrame 对象。如果内容是字符串,则可以通过提供用于序列化它的字符集的 getString 方法来检索它。

默认情况下,ZMQ.CHARSET 用于序列化和反序列化字符串的所有操作。

1
2
ZFrame stringFrame = ZFrame.recvFrame(socket);
String hello = stringFrame.getString(ZMQ.CHARSET);

因为我们利用帧的长度来反映字符串的长度,我们可以通过将每个字符串放入一个单独的帧来发送多个字符串。

在一个消息中发送多个字符串帧是可以使用 sendMore方法的。这个方法将延迟消息的实际发送,直到最后一帧被发送。

1
2
3
socket.sendMore(socket, "HELLO");
socket.sendMore(socket, "beautiful");
socket.send(socket, "WORLD!");

可以使用 ZMsg 类构造多帧消息,而不必使用套接字的 send API。

1
2
3
4
5
ZMsg *strings = new ZMsg();
strings.add("HELLO");
strings.add("beautiful");
strings.add("WORLD");
strings.send(socket);

若要接收一系列字符串帧,请多次调用 recvStr 函数。

1
2
3
String hello     = socket.recvStr();
String beautiful = socket.recvStr();
String world = socket.recvStr();

为了在一次调用中检索整个消息,可以使用 ZMsg 类的静态 recvMsg 方法。

1
2
3
4
ZMsg strings = ZMsg.recvMsg(socket);
String hello = strings.popString();
String beautiful = strings.popString();
String world = strings.popString();

Socket API

套接字实际上是网络编程的标准 API。这就是为什么 ZeroMQ 提供了一个熟悉的基于套接字的API。ZeroMQ 对开发人员特别有吸引力的一点是,它使用不同的套接字类型来实现任何任意的消息传递模式。此外,ZeroMQ 套接字对底层网络协议提供了一个清晰的抽象,这隐藏了这些协议的复杂性,使它们之间的切换非常容易。

与传统套接字的关键区别

一般来说,传统套接字为面向连接的可靠字节流(SOCK_STREAM)或不可靠数据报(SOCK_DGRAM)提供同步接口。相比之下,ZeroMQ 套接字提供了异步消息队列的抽象,其确切的队列语义取决于所使用的套接字类型。传统套接字传输字节流或离散数据报,ZeroMQ 套接字传输离散消息。

ZeroMQ 套接字是异步的,这意味着物理连接的建立和断开、重新连接和有效交付的时间对用户是透明的,并由 ZeroMQ 自己组织。此外,如果对等端无法接收消息,则消息可能被排队。

传统套接字只允许严格的一对一(两个对等体)、多对一(多个客户端、一个服务器)或在某些情况下一对多(多播)关系。除了 PAIR 套接字之外,ZeroMQ 套接字可以连接到多个端点,同时接受来自绑定到套接字的多个端点的入站连接,从而允许多对多关系。

套接字生命

ZeroMQ 套接字生命有四部分,类似 BSD 套接字:

  • 创建和销毁套接字,它们共同形成了一个套接字生命的因果循环;
  • 通过在套接字上设置选项并在必要时检查它们来配置套接字;
  • 通过创建与套接字的ZeroMQ连接,将套接字插入到网络拓扑中;
  • 通过在套接字上写入和接收消息来传递数据。

绑定与连接(Bind vs Connect)

ZeroMQ 套接字并不在乎谁在绑定,谁在连接。在之前的示例中,你可能注意到的是服务端使用了绑定,客户端使用了连接。为什么是这样的,有什么区别?

ZeroMQ 为每个基础的连接创建队列。如果你的套接字连接到了 3 个对等的套接字,那么在后台将有 3 个消息队列。

使用绑定(Bind),允许任意的对等套接字连接(Connect)上来,因此在绑定端不清楚到底会有多少个对等点,也不能提前创建队列。相反,队列是作为连接(Connect)到绑定套接字的单个对等体创建的。

使用连接(Connect),ZeroMQ 知道至少会有一个对等体,因此它可以立即创建单个队列。这适用于所有的套接字类型,除了路由器(ROUTER),只有在我们连接的对等体确认我们的连接后才创建队列。

因此,当向没有对等体的绑定(Bind)套接字或没有实时连接的路由器(ROUTER)发送消息时,没有队列来存储消息。

何时用绑定(Bind),何时用连接(Connect)?

一般情况下,在体系结构最稳定的点(Points)使用绑定(Bind),在具有不稳定端点(Endpoints)的动态组件使用连接(Connect)。对于请求/应答(request/reply),服务提供者可能是您绑定和客户端使用 Connect 的地方。就像普通的旧 TCP 一样。

如果不知道哪个部分更稳定(比如点对点) ,可以考虑中间的一个稳定设备,这样所有的部分都可以去连接。

高水位标记(High-Water-Mark)

高水位线是对 ZeroMQ 在内存中排队的未完成消息的最大数量的硬性限制

如果达到此限制,套接字将进入异常状态,根据套接字类型,ZeroMQ 将采取适当的操作,如阻塞(Blocking)**或丢弃(Dropping)**发送的消息。请参考下面的套接字描述,以了解为每种套接字类型所采取的具体操作。

消息传递模式(Messaging Patterns)

在 ZeroMQ 套接字 API 是对消息传递模式(Messaging Patterns)的包装。ZeroMQ 模式通过具有匹配类型套接字对来实现。

套接字类型定义了套接字的语义、它用于向内向外路由消息的策略、队列等。可以将某些类型的套接字连接在一起,例如,发布服务器套接字和订阅服务器套接字。套接字在“消息传递模式”中协同工作。

内置的 ZeroMQ 核心模式如下:

  • Request-reply(请求/应答),它将一组客户端连接到一组服务端。这是一种远程过程调用(RPC)和任务分布(Task Distribution)模式。

  • Pub-sub(发布-订阅),它将一组发布者连接到一组订阅者。这是一种数据分布(Data Distribution)模式。

  • Pipeline(管道),它以扇出(fan-out)/扇入(fan-in)模式连接节点,该模式可以有多个步骤和循环。这是一个并行任务分配和收集(Parallel task distribution and collection)模式。

  • Exclusive pair(独占对),专门连接两个套接字。这是一种在进程中连接两个线程的模式,不要与“普通”的套接字对混淆。

还有更多ZeroMQ模式仍处于草案状态:

  • Client-server(客户端-服务器),它允许单个 ZeroMQ 服务器与一个或多个 ZeroMQ 客户机通话。客户端始终启动会话,在此之后,任何一方都可以异步地向另一方发送消息。
  • Radio-dish(无线接收天线),用于一对多的数据分发,以扇形方式(fan out)将数据从一个发布者发送到多个订阅者。

Request-reply

https://rfc.zeromq.org/spec/28/

请求-应答(Request-reply)模式适用于各种面向服务的体系结构。它有两种基本形式:** 同步(REQ 和 REP)异步(DEALER 和 ROUTER) **,可以以各种方式混合使用。DEALER 和 ROUTER 套接字是许多高级协议的构建块,比如 rfc.zeromq.org/spec:18/mdp 协议。

REQ

客户端使用 REQ 套接字向服务发送请求并从服务接收响应。这种套接字类型只允许交替的发送和随后的接收调用。一个 REQ 套接字可以连接到任意数量的 REPROUTER 套接字。发送的每个请求都在所有连接的服务之间循环,接收到的每个应答都与最后发出的请求匹配。它是为简单的请求-应答模型而设计的,在这种模型中,针对失败对等体的可靠性不是问题。

如果没有可用的服务,那么套接字上的任何发送操作都将阻塞,直到至少有一个服务可用。REQ 套接字不会丢弃任何消息。

REP

服务使用 REP 套接字接收来自客户机的请求并向客户机发送应答。这种套接字类型只允许接收和随后的发送调用交替序列。接收到的每个请求都是来自所有客户机的公平队列(fair-queued),发送的每个应答都被路由到发出最后一个请求的客户机。如果原始请求者不再存在,则静默丢弃应答。

DEALER

DEALER 套接字类型与一组匿名对等体通信,使用循环(round-robin)算法发送和接收消息。只要它不丢弃消息,它就是可靠的。对于与 REPROUTER 通信的客户端,DEALER 作为 REQ 的异步替代。DEALER 收到的消息从所有连接的对等体公平排队(fair-queued)。

DEALER 由于达到了所有对等体的高水位而进入静音状态,或者如果根本没有对等体,那么套接字上的任何发送操作都将阻塞(Block),直到静音状态结束或至少有一个对等体可用来发送;消息不会被丢弃。

DEALER 连接到 REP 套接字时,发送的消息必须包含一个空帧作为消息的第一部分(分隔符),然后是一个或多个正文部分。

ROUTER

ROUTER 套接字类型与一组对等体对话,使用显式寻址,以便每个传出的消息被发送到一个特定的对等体连接。ROUTER 作为REP的异步替代品,经常用作服务器与 DEALER 通信的基础。

ROUTER 套接字接收消息时,在将消息传递给应用程序之前,将包含原始对等体的路由id的消息部分预先添加到消息部分。接收到的消息从所有连接的对等体中公平排队(fair-queued)。当发送消息时,一个 ROUTER 套接字将删除消息的第一部分,并使用它来确定消息应该被路由到的对等体的路由id。如果对等体已经不存在或从未存在过,则该消息将被静默丢弃

ROUTER 套接字由于达到了对所有对等体的最高水位而进入静音状态时,发送到该套接字的任何消息都将被丢弃,直到静音状态结束。同样,路由到已达到单个高水位标记的对等点的任何消息也将被丢弃。

当一个 REQ 套接字连接到一个 ROUTER 套接字时,除了原始对等体的路由id外,每个收到的消息还应该包含一个空的分隔符消息部分。因此,应用程序看到的每个接收到的消息的整个结构变成:一个或多个路由id部分、分隔符部分、一个或多个主体部分。当向 REQ 套接字发送应答时,应用程序必须包含分隔符部分。

Publish-subscribe

https://rfc.zeromq.org/spec/29/

发布-订阅(Publish-subscribe)模式用于以扇形方式将数据从一个发布者分发到多个订阅者的一对多分布。

ZeroMQ 通过四种套接字类型来支持发布/订阅:PUB,XPUB,SUB,XSUB

Topics

ZeroMQ 使用多部分(Multipart)消息来传递主题(Topic)信息。Topic 表示为字节数组,但也可以使用字符串和适当的文本编码。

发布者必须在消息有效负载之前的第一个帧中包含主题。例如,向status主题的订阅者发布状态消息:

1
2
3
//  Send a message on the 'status' topic
pub.sendMore("Status");
pub.send("All is well");

订阅者通过 Socket 类的 subscribe 方法指定他们感兴趣的主题:

1
2
//  Subscribe to the 'status'
sub.subscribe("status");

一个订阅者(subscriber)套接字可以有多个订阅筛选器(filter)。

使用前缀检查将消息的主题(Topic)与订阅者的订阅主题进行比较,也就是说,订阅了 topic 的订阅者会收到主题为:topictopic/subtopictopical的消息,然而,它不会接收 topiTOPIC 的主题消息。

PUB

发布者使用 PUB 套接字来分发数据。发送的消息以扇出的方式分发到所有连接的对等体。此套接字类型无法接收任何消息。

PUB 套接字由于达到了订阅者的高水位而进入静音状态时,将发送到有问题的订阅者的任何消息都将被丢弃,直到静音状态结束。对于这种套接字类型,Send 函数永远不会阻塞。

SUB

订阅者使用SUB套接字订阅由发布者分发的数据。最初,SUB套接字不订阅任何消息。此套接字类型没有实现 send 函数。

XPUB

PUB 相同,只是可以以传入消息的形式从对等节点接收订阅(Subscription)。订阅消息由两部分组成,一是字节1(用于订阅)或字节0(用于取消订阅),二是在一后面跟着订阅主体。没有订阅或取消订阅前缀的消息也会被接收,但对订阅状态没有影响。

XSUB

SUB相同,不同的是您是通过向套接字发送订阅(Subscription)消息来进行订阅的。订阅消息由两部分组成,一是字节1(用于订阅)或字节0(用于取消订阅),二是在一后面跟着订阅主体。没有订阅或取消订阅前缀的消息也会被发送,但对订阅状态没有影响。

Pipeline

https://rfc.zeromq.org/spec/30/

管道(pipeline)模式用于任务分配,通常是在一个多阶段管道中,其中一个或几个节点将工作推给多个工作人员,然后这些工作人员又将结果推给一个或几个收集器。该模式大多是可靠的,除非节点意外断开连接,否则它不会丢弃消息。它是可扩展的,节点可以在任何时候加入。

ZeroMQ通过两种套接字类型支持流水线:PUSHPULL

PUSH

PUSH 套接字类型与一组匿名的 PULL 对等体对话,使用循环算法发送消息。没有为该套接字类型实现接收(receive)操作。

当由于到达所有下游节点的高水位,或没有下游节点,则套接字进入静音状态,此时任何套接字上发送操作将被阻塞,直到静音状态结束或至少一个下游节点发送可用;消息不会被丢弃

PULL

PULL套接字类型与一组匿名 PUSH 对等体对话,使用公平排队算法接收消息。没有为该套接字类型实现发送(send)操作。

Exclusive Pair

https://rfc.zeromq.org/spec/31/

独占对 PAIR 不是一个通用套接字,用于两个对等体在结构上稳定的特定用例。这通常限制了 PAIR 只能在单个进程中使用,用于线程间通信。

PAIR 类型的套接字在任何时候只能连接到单个对等点。对通过 PAIR 套接字发送的消息不执行路由筛选

当一个 PAIR 套接字由于已连接的对等点达到了高水位标记而进入静音状态,或者如果没有对等点被连接,那么套接字上的任何发送操作都会被阻塞,直到对等点可以发送为止;消息不会被丢弃。

虽然 PAIR 套接字可以通过 inproc 以外的传输方式使用,但是它们不能自动重新连接,而且新连入的连接将被终止,而以前存在的任何连接(包括处于关闭状态的连接)在大多数情况下都不适合 TCP。

zguide

Advanced Request-Reply Pattern

路由的本质是消息中包含了一个身份帧(信封地址)。

REQ 和 REP 是同步的;DEALER 和 ROUTER 是异步的,无视应答信封。

DEALER 类似于异步 REQ 套接字,ROUTER 类似于异步 REP 套接字。在使用 REQ 套接字的地方,我们可以使用 DEALER; 我们只需要自己读写信封。在使用 REP 套接字的地方,我们可以粘贴 ROUTER; 我们只需要自己管理标识。

可以将 REQ 和 DEALER 套接字视为“客户端”,将 REP 和 ROUTER 套接字视为“服务器”。通常,您需要 bind REP 和 ROUTER 套接字,并将 REQ 和 DEALER 套接字 connect 到它们。

Identity 标识当前套接字的身份信息;envelope(Envelope Frame) 标识将要路由到的套接字地址

请求-应答模式套接字有效的组合:

  • REQ to REP
  • DEALER to REP
  • REQ to ROUTER
  • DEALER to ROUTER
  • DEALER to DEALER
  • ROUTER to ROUTER

无效组合:

  • REQ to REQ 双方都希望通过相互发送消息来开始。
  • REQ to DEALER 当有多个 REQ 时,DEALER 无法原路返回。
  • REP to REP 双方都会等待对方发送第一条消息。
  • REP to ROUTER ROUTER 不知道 REP 的地址。