ZeroMQ(也称为ØMQ,0MQ或ZMQ)是一种高性能的异步消息传递库,旨在用于分布式或并行应用程序中。它提供了一个消息队列,但是与面向消息的中间件不同,ZeroMQ系统可以在没有专用消息代理的情况下运行。
ZeroMQ通过各种传输(TCP,进程内,进程间,多播,WebSocket等)支持通用消息传递模式(发布/订阅,请求/回复,客户端/服务器等),从而使进程间消息传递变得简单作为线程间消息传递。这样可以使您的代码清晰,模块化并且易于扩展。
ZeroMQ由大型贡献者社区开发。许多流行的编程语言都有第三方绑定,而C#和Java有本地端口。
ZMQ(ØMQ、ZeroMQ, 0MQ)看起来像是一套嵌入式的网络链接库,但工作起来更像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的快速足以胜任集群应用产品。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。ZMQ有着多语言支持,并能在几乎所有的操作系统上运行。ZMQ是iMatix公司的产品,以LGPL开源协议发布。
需要具备的知识
- 使用最新的ZMQ稳定版本;
- 使用Linux系统或其他相似的操作系统;
- 能够阅读C语言代码,这是本指南示例程序的默认语言;
- 当我们书写诸如PUSH或SUBSCRIBE等常量时,你能够找到相应语言的实现,如ZMQ_PUSH、ZMQ_SUBSCRIBE。
ZeroMQ中的零
ZeroMQ的理念从零开始。零表示零代理(ZeroMQ是无代理),零延迟,零成本(免费)和零管理。
更广泛地说,“零”是指渗透到项目中的极简主义文化。我们通过消除复杂性而不是通过公开新功能来增加功能。
该指南解释了如何使用ØMQ,涵盖了28种语言的60多种图表和750个示例的基本,中间和高级用法。
也可以作为O'Reilly的书获得。
支持平台
Libzmq主要用C ++ 98编写,带有一些可选的C ++ 11片段。对于配置,可以使用自动工具或CMake。请参阅下面的一些平台列表,其中已成功编译libzmq。
获取示例
本指南的所有示例都存放于github仓库中,最简单的获取方式是运行以下代码:
git clone git://github.com/imatix/zguide.git
浏览examples目录,你可以看到多种语言的实现。如果其中缺少了某种你正在使用的语言,我们很希望你可以提交一份补充。这也是本指南实用的原因,要感谢所有做出过贡献的人。
所有的示例代码都以MIT/X11协议发布,若在源代码中有其他限定的除外。
ZeroMQ低级库-libzmq
Libzmq(https://github.com/zeromq/libzmq)是大多数不同语言绑定背后的低级库。Libzmq公开了C-API并以C ++实现。您很少会直接使用libzmq,但是,如果您想为项目做贡献或学习zeromq的内部知识,那是开始的地方。https://github.com/zeromq/libzmq
第一个例子
因此,让我们从一些代码开始,当然是“ Hello world”示例。
// Hello World server
#include <czmq.h>
int main (void)
{
// Socket to talk to clients
zsock_t *responder = zsock_new (ZMQ_REP);
int rc = zsock_bind (responder, "tcp://*:5555");
assert (rc == 0);
while (1) {
char *str = zstr_recv (responder);
printf ("Received Hello\n");
sleep (1); // Do some 'work'
zstr_send (responder, "World");
zstr_free (&str);
}
return 0;
}
服务器创建一个类型为response的套接字(您将在稍后阅读有关请求-响应的更多信息 ),将其绑定到端口5555,然后等待消息。您还可以看到我们的配置为零,我们只是发送字符串。
// Hello World client
#include <czmq.h>
int main (void)
{
printf ("Connecting to hello world server…\n");
zsock_t *requester = zsock_new (ZMQ_REQ);
zsock_connect (requester, "tcp://localhost:5555");
int request_nbr;
for (request_nbr = 0; request_nbr != 10; request_nbr++) {
printf ("Sending Hello %d…\n", request_nbr);
zstr_send (requester, "Hello");
char *str = zstr_recv (requester);
printf ("Received World %d\n", request_nbr);
zstr_free (&str);
}
zsock_destroy (&requester);
return 0;
}
客户端创建一个请求类型的套接字,连接并开始发送消息。
两个send
和receive
方法阻断(默认)。对于接收来说很简单:如果没有消息,该方法将阻塞。对于发送,它更加复杂,并且取决于套接字类型。对于请求套接字,如果达到高水位标记或没有连接对等方,则该方法将阻塞。
ZeroMQ消息是在应用程序或同一应用程序的组件之间传递的离散数据单元。从ZeroMQ本身的角度来看,消息被认为是不透明的二进制数据。
在线上,ZeroMQ消息是可容纳在内存中的大小从零到零的任何大小的Blob。您可以使用协议缓冲区,msgpack,JSON或您的应用程序需要说的其他任何东西来进行自己的序列化。选择可移植的数据表示是明智的,但是您可以自行权衡取舍。
最简单的ZeroMQ消息由一帧组成(也称为消息部分)。帧是ZeroMQ消息的基本连线格式。帧是长度指定的数据块。长度可以向上为零。ZeroMQ保证传递消息的所有部分(一个或多个),或不发送任何部分。这使您可以将发送或接收帧列表作为一条在线消息发送或接收。
一条消息(单部分或多部分)必须适合内存。如果要发送任意大小的文件,则应将它们分成多个部分,并将每个部分作为单独的单部分消息发送。使用多部分数据不会减少内存消耗。
使用字符串
ZMQ不会关心发送消息的内容,只要知道它所包含的字节数。所以,程序员需要做一些工作,保证对方节点能够正确读取这些消息。如何将一个对象或复杂数据类型转换成ZMQ可以发送的消息,这有类似Protocol Buffers的序列化软件可以做到。但对于字符串,你也是需要有所注意的。
将数据作为字符串传递通常是使通信继续进行的最简单方法,因为序列化非常简单。对于ZeroMQ,我们建立了以下规则:字符串是长度指定的,并在线路上发送,且不带尾随null。
在C语言中,字符串都以一个空字符结尾,你可以像这样发送一个完整的字符串:
zmq_msg_init_data (&request, "Hello", 6, NULL, NULL);
以下函数将C字符串作为单帧消息发送到套接字,其中字符串的长度等于帧的长度。
zstr_send (socket, "HELLO");
另一种选择是发送一个格式化字符串,类似于printf。
zstr_sendf (socket, "%s-%d", "HELLO", 1);
要从套接字读取字符串,只需调用该zstr_recv
函数。
char *string = zstr_recv (socket);
因为我们利用框架的长度来反映字符串的长度,所以我们可以通过将多个字符串放入单独的框架中来发送多个字符串。
使用该zstr_sendm
功能可以发送多个字符串帧。此功能将推迟实际发送消息的时间,直到最后一帧准备就绪为止。
zstr_sendm (socket, "HELLO");
zstr_sendm (socket, "beautiful");
zstr_send (socket, "WORLD!");
或者使用此zstr_sendx
功能甚至更简单。最后一个参数必须为NULL!
zstr_sendx (socket, "HELLO", "beautiful", "WORLD!", NULL);
如果您喜欢使用消息对象,而不是通过发送多个帧来构建消息。您可以使用zmsg
该类。
zmsg_t *strings = zmsg_new ();
zmsg_addstr ("HELLO");
zmsg_addstr ("beautiful");
zmsg_addstr ("WORLD");
zmsg_send (&strings, socket);
要接收一系列字符串帧,请使用zstr_recvx
函数。每个字符串都被分配并填充有字符串数据!
char *hello, beautiful, world;
zstr_recvx (socket, &hello, &beautiful, &world, NULL);
或者,如果您正在使用zmsg
:
zmsg_t *strings = zmsg_recv (socket);
char *hello = zmsg_popstr (strings);
char *beautiful = zmsg_popstr (strings);
char *world = zmsg_popstr (strings);
了解有关使用消息的更多信息:
如果你从C语言中读取该消息,你会读到一个类似于字符串的内容,甚至它可能就是一个字符串(第六位在内存中正好是一个空字符),但是这并不合适。这样一来,客户端和服务端对字符串的定义就不统一了,你会得到一些奇怪的结果。
当你用C语言从ZMQ中获取字符串,你不能够相信该字符串有一个正确的结尾。因此,当你在接受字符串时,应该建立多一个字节的缓冲区,将字符串放进去,并添加结尾。
所以,让我们做如下假设:ZMQ的字符串是有长度的,且传送时不加结束符。在最简单的情况下,ZMQ字符串和ZMQ消息中的一帧是等价的,就如上图所展现的,由一个长度属性和一串字节表示。
下面这个功能函数会帮助我们在C语言中正确的接受字符串消息:
// 从ZMQ套接字中接收字符串,并转换为C语言的字符串
static char *
s_recv (void *socket) {
zmq_msg_t message;
zmq_msg_init (&message);
zmq_recv (socket, &message, 0);
int size = zmq_msg_size (&message);
char *string = malloc (size + 1);
memcpy (string, zmq_msg_data (&message), size);
zmq_msg_close (&message);
string [size] = 0;
return (string);
}
这段代码我们会在日后的示例中使用,我们可以顺手写一个s_send()方法,并打包成一个.h文件供我们使用。
这就诞生了zhelpers.h,一个供C语言使用的ZMQ功能函数库。它的源代码比较长,而且只对C语言程序员有用,你可以在闲暇时看一看。
获取版本号
ZMQ目前有多个版本,而且仍在持续更新。如果你遇到了问题,也许这在下一个版本中已经解决了。想知道目前的ZMQ版本,你可以在程序中运行如下:
version: ØMQ version reporting in C
//
// 返回当前ZMQ的版本号
//
#include "zhelpers.h"
int main (void)
{
int major, minor, patch;
zmq_version (&major, &minor, &patch);
printf ("当前ZMQ版本号为 %d.%d.%d\n", major, minor, patch);
return EXIT_SUCCESS;
}
三种基本模型
https://www.jianshu.com/p/c5e191d58ae5
ZMQ 提供了三种基本的通信模型,分别是 Request-Reply 、Publish-Subscribe 和 Parallel Pipeline ,接下来举例说明三种模型并给出相应的代码实现。
Request-Reply(请求-回复)
以 “Hello World” 为例。客户端发起请求,并等待服务端回应请求。客户端发送一个简单的 “Hello”,服务端则回应一个 “World”。可以有 N 个客户端,一个服务端,因此是 1-N 连接。
从以上的过程,我们可以了解到使用 ZMQ 写基本的程序的方法,需要注意的是:
- 服务端和客户端无论谁先启动,效果是相同的,这点不同于 Socket。
- 在服务端收到信息以前,程序是阻塞的,会一直等待客户端连接上来。
- 服务端收到信息后,会发送一个 “World” 给客户端。值得注意的是一定是客户端连接上来以后,发消息给服务端,服务端接受消息然后响应客户端,这样一问一答。
- ZMQ 的通信单元是消息,它只知道消息的长度,并不关心消息格式。因此,你可以使用任何你觉得好用的数据格式,如 Xml、Protocol Buffers、Thrift、json 等等。
hwserver.c: Hello World server
//
// Hello World 服务端
// 绑定一个REP套接字至tcp://*:5555
// 从客户端接收Hello,并应答World
//
#include <zmq.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main (void)
{
void *context = zmq_init (1);
// 与客户端通信的套接字
void *responder = zmq_socket (context, ZMQ_REP);
zmq_bind (responder, "tcp://*:5555");
while (1) {
// 等待客户端请求
zmq_msg_t request;
zmq_msg_init (&request);
zmq_recv (responder, &request, 0);
printf ("收到 Hello\n");
zmq_msg_close (&request);
// 做些“处理”
sleep (1);
// 返回应答
zmq_msg_t reply;
zmq_msg_init_size (&reply, 5);
memcpy (zmq_msg_data (&reply), "World", 5);
zmq_send (responder, &reply, 0);
zmq_msg_close (&reply);
}
// 程序不会运行到这里,以下只是演示我们应该如何结束
zmq_close (responder);
zmq_term (context);
return 0;
}
使用REQ-REP套接字发送和接受消息是需要遵循一定规律的。客户端首先使用zmq_send()发送消息,再用zmq_recv()接收,如此循环。如果打乱了这个顺序(如连续发送两次)则会报错。类似地,服务端必须先进行接收,后进行发送。
hwclient: Hello World client in C
//
// Hello World 客户端
// 连接REQ套接字至 tcp://localhost:5555
// 发送Hello给服务端,并接收World
//
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main (void)
{
void *context = zmq_init (1);
// 连接至服务端的套接字
printf ("正在连接至hello world服务端...\n");
void *requester = zmq_socket (context, ZMQ_REQ);
zmq_connect (requester, "tcp://localhost:5555");
int request_nbr;
for (request_nbr = 0; request_nbr != 10; request_nbr++) {
zmq_msg_t request;
zmq_msg_init_size (&request, 5);
memcpy (zmq_msg_data (&request), "Hello", 5);
printf ("正在发送 Hello %d...\n", request_nbr);
zmq_send (requester, &request, 0);
zmq_msg_close (&request);
zmq_msg_t reply;
zmq_msg_init (&reply);
zmq_recv (requester, &reply, 0);
printf ("接收到 World %d\n", request_nbr);
zmq_msg_close (&reply);
}
zmq_close (requester);
zmq_term (context);
return 0;
}
这看起来是否太简单了?ZMQ就是这样一个东西,你往里加点儿料就能制作出一枚无穷能量的原子弹,用它来拯救世界吧!
理论上你可以连接千万个客户端到这个服务端上,同时连接都没问题,程序仍会运作得很好。你可以尝试一下先打开客户端,再打开服务端,可以看到程序仍然会正常工作,想想这意味着什么。
让我简单介绍一下这两段程序到底做了什么。首先,他们创建了一个ZMQ上下文,然后是一个套接字。不要被这些陌生的名词吓到,后面我们都会讲到。服务端将REP套接字绑定到5555端口上,并开始等待请求,发出应答,如此循环。客户端则是发送请求并等待服务端的应答。
这些代码背后其实发生了很多很多事情,但是程序员完全不必理会这些,只要知道这些代码短小精悍,极少出错,耐高压。这种通信模式我们称之为请求-应答模式,是ZMQ最直接的一种应用。你可以拿它和RPC及经典的C/S模型做类比。
ZMQ使用C语言作为它参考手册的语言,本指南也以它作为示例程序的语言。如果你正在阅读本指南的在线版本,你可以看到示例代码的下方有其他语言的实现。如以下是C++语言:
hwserver.cpp: Hello World server
//
// Hello World 服务端 C++语言版
// 绑定一个REP套接字至tcp://*:5555
// 从客户端接收Hello,并应答World
//
#include <zmq.hpp>
#include <string>
#include <iostream>
#include <unistd.h>
int main () {
// 准备上下文和套接字
zmq::context_t context (1);
zmq::socket_t socket (context, ZMQ_REP);
socket.bind ("tcp://*:5555");
while (true) {
zmq::message_t request;
// 等待客户端请求
socket.recv (&request);
std::cout << "收到 Hello" << std::endl;
// 做一些“处理”
sleep (1);
// 应答World
zmq::message_t reply (5);
memcpy ((void *) reply.data (), "World", 5);
socket.send (reply);
}
return 0;
}
可以看到C语言和C++语言的API代码差不多,而在PHP这样的语言中,代码就会更为简洁:
hwserver.php: Hello World server
<?php
/**
* Hello World 服务端
* 绑定REP套接字至 tcp://*:5555
* 从客户端接收Hello,并应答World
* @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
*/
$context = new ZMQContext(1);
// 与客户端通信的套接字
$responder = new ZMQSocket($context, ZMQ::SOCKET_REP);
$responder->bind("tcp://*:5555");
while(true) {
// 等待客户端请求
$request = $responder->recv();
printf ("Received request: [%s]\n", $request);
// 做一些“处理”
sleep (1);
// 应答World
$responder->send("World");
}
Publish-Subscribe(发布-订阅)
下面以一个天气预报的例子来介绍该模式。
服务端不断地更新各个城市的天气,客户端可以订阅自己感兴趣(通过一个过滤器)的城市的天气信息。
需要注意的是:
- 与 “Hello World” 例子不同的是,Socket 的类型变成 ZMQ.PUB 和 ZMQ.SUB 。
- 客户端需要设置一个过滤条件,接收自己感兴趣的消息。
- 发布者一直不断地发布新消息,如果中途有订阅者退出,其他均不受影响。当订阅者再连接上来的时候,收到的就是后来发送的消息了。这样比较晚加入的或者是中途离开的订阅者必然会丢失掉一部分信息。这是该模式的一个问题,即所谓的 "Slow joiner" 。
Parallel Pipeline - 并行管道
Parallel Pipeline 处理模式如下:
- ventilator 分发任务到各个 worker
- 每个 worker 执行分配到的任务
- 最后由 sink 收集从 worker 发来的结果
以下两点需要注意:
- ventilator 使用 ZMQ.PUSH 来分发任务;worker 用 ZMQ.PULL 来接收任务,用 ZMQ.PUSH 来发送结果;sink 用 ZMQ.PULL 来接收 worker 发来的结果。
- ventilator 既是服务端,也是客户端(此时服务端是 sink);worker 在两个过程中都是客户端;sink 也一直都是服务端。