神器篇之ZeroMQ<一>初识ZeroMQ

来源:互联网 发布:矩阵行列式的性质 编辑:程序博客网 时间:2024/04/29 11:55

//////////////////////////////////////////////////////////////////////////////////////////////////

Email: admin@ofssln.com

QQ:285125896

欢迎一起交流,学习。

////////////////////////////////////////////////////////////////////////////////////////////////////

云计算时代最好的通讯库,没有之一。

还在学socket编程吗?还在研究为什么epoll比select更好吗?
噢,不必了!
在复杂的云计算环境中,我们面临的难题远比这个复杂得多。任何一个简单的问题放之大数据下,都是难题。低廉的服务器构建一个高可用且具备伸缩能力的云计算环境;计算节点的热插拔;大并发;负载均衡等。

1.为什么说ZeroMQ是云计算时代最好的通讯库呢?我们从ZeroMQ的特性来分析吧:

1.1.The socket library that acts as a concurrency framework.

开起来像是并行开发框架的socket库。
为什么一个通讯的库不提供socket的风格,反而看起来像是一个并行的库?
云计算不就是分布式计算嘛!
并行、多核、分布式,让计算能力不断的被扩展扩展,让数据不断地被分区分区,强大的计算能力就是这样堆出来的。
并发是目前云计算这个世界的主题,所以ZMQ提供了一个并发的库,正式我们最最需要的。
如同广告所讲:客户要的不是一英寸的钻头,而是一英寸的洞。
我们要的不是通讯,而是分布式并行计算。
1.2. Carries messages across inproc, IPC, TCP, and multicast.
提供进程内、进程间、机器间和广播方式的消息通讯。
可以说ZMQ提供了一种强大的复杂环境适应能力。
作为一个通讯库,可能我们觉得进程内通讯和进程间通讯不是重要的。
然而,提供这些功能,使得ZMQ能够在特定的场景下提供特定的解决方案。且通讯的配置相当的简单:inproc://, ipc://, tcp://这三个通讯方案简单地在字符串中指定即可。开发者可以很容易开发出可运维的应用程序,在不同的场景下,可以仅修改配置文件来适应复杂的部署环境。
1.3.Connect N-to-N via fanout, pubsub, pipeline, request-reply.
在多对多的网络环境中提供多对一,发布/订阅(one-to-many),管道(one-to-one),请求/响应等模型。
模式,还是模式。
天天做网络的开发的人,可能会觉得通讯就那么三板斧,经典的模式不断在重复,可是我们仍然在具体的问题上反复写着类似的代码。而ZMQ提供的不仅仅是这个:ZMQ就像一堆水管的转接头,在复杂的自来水供水系统中,ZMQ在每个关节灵活地适配,像水管一样接起来,把数据分开或是合并。
例如,先把数据按照pub/sub模式分发给多个服务器,每个服务器上的进程在进程内用inproc,将请求分布到多个线程上处理,如果有特别的需要,还可以把数据用ipc方式转发给同一机器上的其他进程。而完成这一切复杂的工作仅需要少少的代码。
1.4. Fast enough for clustered products and supercomputing.
对服务器群集和超级计算来将都足够快了
超级计算都能做,你还想干啥?
1.5. Asynch I/O for scalable multicore message-passing apps.
对可扩展的多核消息传递应用程序提供异步I/O支持。
在ZMQ的inproc://模式中,库提供了线程安全的消息分发机制,可以简单地把请求分发给多线程处理。
1.6. Large and active open source community.
拥有超大并且活跃的开源社区
记住,你不是一个人在战斗!不是……
1.7. 20+ languages including C, C++, Java, .NET, Python.
有超过20种以上的开发语言绑定,诸如C, C++, Java, .NET, Python
1.8. Most OSes including Linux, Windows, OS X.
还支持绝大多数的操作系统,例如Linux, Windows, OS X
1.9. LGPL free software with full commercial support.
这是最重要的,不要钱,但也可以提供商业支持。


2.我们怎么来学习ZeroMQ呢?

ZMQ希望用户在编写应用程序方面,更加关心消息通信而不是socket连接。在内部已经使用异步方式帮用户处理好了网络IO读写以及数据成帧,使得用户在处理的时候只需要关注消息本身。 ZMQ所支持的消息传输模型有很多种,比如push-pull,pub-sub,request-reply以及exclusive-pair,应该可以涵盖应用层面所有的消息通讯方式。同时ZMQ本身可以底层允许选择不同的通信协议, 比如tcp,ipc(进程间通信),inproc(线程间通信).

2.1首先阅读一下manual.然后看看guide里面包括很多设计细节和使用细节东西。最后可以阅读一下API reference.关于Background方面的内容也有一些whitepapers可以参考。

http://www.zeromq.org/intro:read-the-manual
http://zguide.zeromq.org/page:all
http://api.zeromq.org/2-1:_start
http://www.zeromq.org/whitepapers:multithreading-magic

2.2 ZMQ总结了几种消息通信模型可以覆盖所有的应用程序,分别是下面这几种:
Exclusive-Pair
Publish-Subscribe
Push-Pull
Request-Reply

我们会在后面仔细讨论每一种消息通信模型

使用ZMQ之后的话不像我们所认识一样的server必须在client之前启动。在ZMQ下面的话client完全可以在server之前启动。一旦连接顺序并不按照我们所想象的那样工作的话,连接顺序是无关的时候,那么客户端和服务端这两个概念就非常模糊了。我们必须重新考虑什么是server,什么是client.ZMQ给出一个非常实际的答案。我们将socket嵌入到网络拓扑的时候,server应该是网络结构中稳定的部分,而client应该是网络结构中比较易变的部分。

2.3Example

首先我们看看ZMQ的使用情况。我们以简单的Request-Reply消息模型为例。所谓Request-Reply模型就是client发送一个消息然后等待回应,而server接收到消息然后立刻回复。 这是一个lockstep的过程,就是说中途不能够切换做另外的事情,比如client发送消息之后就不能够再发送必须等待回应,而server接收到消息之后必须立刻恢复而不能够再次接收。 咋一看这是一个同步过程,但是后面可以看到ZMQ允许完成异步的Request-Reply.使用这种简单的通信模型我们可以很快地写出一个echo服务。

//============================================================//client.cc.send 'hello' always to server,which hosts on localhost:19870//============================================================#include <zmq.h>#include <string.h>#include <stdio.h>#include <unistd.h>int main() {    void* ctx=zmq_init(1);    printf("connect to server 'localhost:19870'\n");    void* req=zmq_socket(ctx,ZMQ_REQ);    zmq_connect(req,"tcp://localhost:19870");    for(int i=0; i<10; i++) {        zmq_msg_t request;        zmq_msg_init_size(&request,6);        //including trailing '\0'.        memcpy(zmq_msg_data(&request),"hello",6);        printf("[C]send 'hello'\n");        // 0 means in block way.        zmq_send(req,&request,0);        zmq_msg_close(&request);        zmq_msg_t reply;        zmq_msg_init(&reply);        // 0 means in block way.        zmq_recv(req,&reply,0);        printf("[C]recv '%s'\n",zmq_msg_data(&reply));        zmq_msg_close(&reply);    }    zmq_close(req);    zmq_term(ctx);    return 0;}

//============================================================//server.cc which hosts on *:19870//============================================================#include <zmq.h>#include <string.h>#include <stdio.h>#include <unistd.h>int main() {    void* ctx=zmq_init(1);    printf("bind to '*:19870'\n");    void* rep=zmq_socket(ctx,ZMQ_REP);    zmq_bind(rep,"tcp://*:19870");    while(1){        zmq_msg_t request;        zmq_msg_init(&request);        // 0 means in block way        zmq_recv(rep,&request,0);        const char* s=(const char*)zmq_msg_data(&request);        printf("[S]recv '%s'\n",s);        zmq_msg_t reply;        // include trailing '\0'.        zmq_msg_init_size(&reply,strlen(s)+1);        memcpy(zmq_msg_data(&reply),s,strlen(s)+1);        zmq_msg_close(&request);        // 0 means in block way.        printf("[S]send '%s'\n",s);        zmq_send(rep,&reply,0);        zmq_msg_close(&reply);    }    zmq_close(rep);    zmq_term(ctx);    return 0;}

从编写角度来看的话确实简化了不少

zmq_init创建一个context.这个context就可以认为是一个MQ实例。1表示IO线程数。
zmq_socket根据context来创建一个socket,后面类型指定了MQ通信类型。
zmq_bind/zmq_connect可以进行绑定进行监听或者是进行连接。
zmq_msg_init/zmq_msg_init_size可以用来初始化一个message
zmq_send/zmq_recv可以进行message的发送和接收。
zmq_msg_close销毁一个message
zmq_close关闭一个socket
zmq_term销毁一个context

ZMQ底层做好了poller机制,对于server来说的话将多个connection映射到一个socket上面来了。底层使用其他线程完成了IO读写。 这里可以看到如果使用TCP的话底层应该是字节流,而我们没有指定任何成帧策略就得到了一条条消息,可以看到ZMQ内置有一个字节流成帧策略。

2.4 Protocol

我们从上面的Example里面看到,在进行zmq_bind/zmq_connect的时候指定了通信地址,而通信地址上面还附带了通信协议"tcp".ZMQ本身是允许工作在多种通信协议上面的:
tcp // tcp
ipc // 进程间通信。猜想底层应该是unix domain socket实现的.因为运行完毕之后我们可以看到socket文件。
inproc // 线程间通信。对于这种通讯协议来说的话底层IO线程没用使用。
pgm // ???
epgm // ???

我们可以非常容易地切换到其他通信协议上,而不需要修改任何代码。


2.5Message

我们从上面的Example可以看到,ZMQ内部有一个默认的成帧策略,也就是说我们使用zmq_recv/zmq_send这样写成的webserver是不能够正常工作的, 因为zmq_recv/zmq_send只能够处理内置的消息格式,而不能够处理http请求这种字节流,按照文档的说法"ZMQ is not a neutral carrier".

ZMQ的消息格式是这样的:

<span style="font-family:Microsoft YaHei;"><span style="white-space:pre"></span>struct msg{    <span style="white-space:pre"></span>msg_size_t size; // 但是为了效率的话会使用特殊的方法进行压缩    <span style="white-space:pre"></span>msg_data_t data[0];<span style="white-space:pre"></span>};</span>
ZMQ允许一条message按照多个部分进行发送(multipart message),为了能够更好地描述这节的话我们重新定义一些名词。 后面我们可能会混用这两个名词,但是读者应该是可以区分的:
frame.single part message.
message.多个frame组成的一条完整message.

我们使用下面的例子来说明如何进行multipart message传输和接收的。multipart message对于理解后面的路由非常重要。

//  Convert C string to ZMQ string and send to socketstatic ints_send (void *socket, char *string) {    int rc;    zmq_msg_t message;    zmq_msg_init_size (&message, strlen (string));    memcpy (zmq_msg_data (&message), string, strlen (string));    rc = zmq_send (socket, &message, 0);    zmq_msg_close (&message);    return (rc);}//  Sends string as ZMQ string, as multipart non-terminalstatic ints_sendmore (void *socket, char *string) {    int rc;    zmq_msg_t message;    zmq_msg_init_size (&message, strlen (string));    memcpy (zmq_msg_data (&message), string, strlen (string));    rc = zmq_send (socket, &message, ZMQ_SNDMORE);    zmq_msg_close (&message);    return (rc);}//  Receives all message parts from socket, prints neatly//static voids_dump (void *socket){    puts ("----------------------------------------");    while (1) {        //  Process all parts of the message        zmq_msg_t message;        zmq_msg_init (&message);        zmq_recv (socket, &message, 0);        //  Dump the message as text or binary        char *data = (char*) zmq_msg_data (&message);        int size = zmq_msg_size (&message);        int is_text = 1;        int char_nbr;        for (char_nbr = 0; char_nbr < size; char_nbr++)            if ((unsigned char) data [char_nbr] < 32            ||  (unsigned char) data [char_nbr] > 127)                is_text = 0;        printf ("[%03d] ", size);        for (char_nbr = 0; char_nbr < size; char_nbr++) {            if (is_text)                printf ("%c", data [char_nbr]);            else                printf ("%02X", (unsigned char) data [char_nbr]);        }        printf ("\n");        int64_t more;           //  Multipart detection        size_t more_size = sizeof (more);        zmq_getsockopt (socket, ZMQ_RCVMORE, &more, &more_size);        zmq_msg_close (&message);        if (!more)            break;      //  Last message part    }}
如果使用ZMQ出现消息丢失的话,那么可以按照下面这个solver来查找原因 http://zguide.zeromq.org/page:all#Missing-Message-Problem-Solver .

2..6 Identity

Identity可以用来表示一个socket的身份,对于ZMQ是非常有用途的,现在能够总结到的作用有下面这些:
持久化socket(durable socket).影响到Publish-Subscribe通信模型的可靠性。
路由(routing).影响到ROUTER的路由选择。
zmq_setsockopt(socket,ZMQ_IDENTITY,"www.ofssln.com",5);

如果没有设置Identity的话,那么在pub-sub模型上的话就会出现消息丢失,而在路由的时候那么ROUTER会帮助用户生成UUID. Identity的实现非常简单,就是整个message开头加上一个特殊的frame来标记的。

2.7 Device

一旦通信节点超过一定数量的话,那么最好需要一个转发节点或者是中间节点,不然通信费用以及管理复杂度都会急剧上升。作为一个转发节点来说的话, 逻辑非常简单,从一个socket读取数据,然后向另外一个socket里面写数据,可以认为类似于pipe这样的机制。在ZMQ里面称这样的节点为Device. ZMQ里面内置的Device有下面三种:

  • QUEUE, which is like the request-reply broker. http://zguide.zeromq.org/page:all#A-Request-Reply-Broker .
  • FORWARDER, which is like the pub-sub proxy server. http://zguide.zeromq.org/page:all#A-Publish-Subscribe-Proxy-Server .
  • STREAMER, which is like FORWARDER but for pipeline flows.

使用device也非常简单.

#include "zhelpers.h"int main (void){    void *context = zmq_init (1);    // Socket facing clients    void *frontend = zmq_socket (context, ZMQ_ROUTER);    zmq_bind (frontend, "tcp://*:5559");    // Socket facing services    void *backend = zmq_socket (context, ZMQ_DEALER);    zmq_bind (backend, "tcp://*:5560");    // Start built-in device    zmq_device (ZMQ_QUEUE, frontend, backend);    // We never get here…    zmq_close (frontend);    zmq_close (backend);    zmq_term (context);    return 0;}
ZMQ Guide里面提到了不要将不同Device和socket进行混用. If you're like most 01MQ users, at this stage your mind is starting to think, "what kind of evil stuff can I do if I plug random socket types into devices?" The short answer is: don't do it. You can mix socket types but the results are going to be weird. So stick to using ROUTER/DEALER for queue devices, SUB/PUB for forwarders and PULL/PUSH for streamers. 但是如果实际阅读代码的话,会发现这个部分的逻辑都是一样的,也就是事实上在现在ZMQ版本里面是可以混用的

int zmq_device (int device_, void *insocket_, void *outsocket_){    if (!insocket_ || !outsocket_) {        errno = EFAULT;        return -1;    }    if (device_ != ZMQ_FORWARDER && device_ != ZMQ_QUEUE &&          device_ != ZMQ_STREAMER) {       errno = EINVAL;       return -1;    }    return zmq::device ((zmq::socket_base_t*) insocket_,        (zmq::socket_base_t*) outsocket_);}

而zmq::device逻辑也非常简单,就是之前提到pipe工作机制。内部使用了ZMQ本身提供的zmq_poll机制来进行通知哪个socket上面有数据。

int zmq::device (class socket_base_t *insocket_,        class socket_base_t *outsocket_){    zmq_msg_t msg;    int rc = zmq_msg_init (&msg);    if (rc != 0) {        return -1;    }    int64_t more;    size_t moresz;    zmq_pollitem_t items [2];    items [0].socket = insocket_;    items [0].fd = 0;    items [0].events = ZMQ_POLLIN;    items [0].revents = 0;    items [1].socket = outsocket_;    items [1].fd = 0;    items [1].events = ZMQ_POLLIN;    items [1].revents = 0;    while (true) {        //  Wait while there are either requests or replies to process.        rc = zmq_poll (&items [0], 2, -1);        if (unlikely (rc < 0)) {            return -1;        }        //  The algorithm below asumes ratio of request and replies processed        //  under full load to be 1:1. Although processing requests replies        //  first is tempting it is suspectible to DoS attacks (overloading        //  the system with unsolicited replies).        //  Process a request.        if (items [0].revents & ZMQ_POLLIN) {            while (true) {                rc = insocket_->recv (&msg, 0);                if (unlikely (rc < 0)) {                    return -1;                }                moresz = sizeof (more);                rc = insocket_->getsockopt (ZMQ_RCVMORE, &more, &moresz);                if (unlikely (rc < 0)) {                    return -1;                }                rc = outsocket_->send (&msg, more ? ZMQ_SNDMORE : 0);                if (unlikely (rc < 0)) {                    return -1;                }                if (!more)                    break;            }        }        //  Process a reply.        if (items [1].revents & ZMQ_POLLIN) {            while (true) {                rc = outsocket_->recv (&msg, 0);                if (unlikely (rc < 0)) {                    return -1;                }                moresz = sizeof (more);                rc = outsocket_->getsockopt (ZMQ_RCVMORE, &more, &moresz);                if (unlikely (rc < 0)) {                    return -1;                }                rc = insocket_->send (&msg, more ? ZMQ_SNDMORE : 0);                if (unlikely (rc < 0)) {                    return -1;                }                if (!more)                    break;            }        }    }    return 0;}

2.8 Congestion

ZMQ可以通过控制HWM(high-water mark)来控制拥塞。内部实现上每一个socket有关联了buffer,HWM可以控制buffer大小

  • PUB/PUSH有transmit buffers.
  • SUB/PULL/REQ/REP有receive buffers.
  • DEALER/ROUTER/PAIR有transmit buffers也有receive buffers.

一旦socket达到了high-water mark的话,那么会根据socket类型来决定是丢弃还是block.现在实现而言的话PUB会尝试丢弃数据,而其他类型的socket就会block住。 如果socket是线程之间进行通信的话,那么HWM是两者socket的HWM之和。因为默认HWM是ulimited的,所以只要一端没有设置的话那么容量就无限。


如果我们的内存有限的话那么我们希望将内存swap到磁盘上面。ZMQ允许我们如果拥塞内存超过HWM的话,那么还可以将内存swap到磁盘上面去。 不过这个磁盘内容我们是不可见的,并且不能够进行持久化。如果进程一旦crash重启的话那么内容消失。仅仅是为了swap用的,而不是为了持久化用的


2.9Exclusive-Pair

Exclusive-Pair是最简单的1:1通信模式,你可以认为就是一个TCPConnection.我们依然需要写bind和connect,但是server只能够接受一个连接。 数据可以进行双向连接,没有类似于REQ-REP的lockstep这样的要求。例子中我们连续发送了两个message,然后使用了inproc协议的socket.

#include <zmq.h>#include <cstdio>#include <cstdlib>#include <cstring>#include <pthread.h>void* second(void* arg){    void* ctx=arg;    void* pair=zmq_socket(ctx,ZMQ_PAIR);    zmq_connect(pair,"inproc://channel");    for(int i=0;i<2;i++){        zmq_msg_t msg;        zmq_msg_init(&msg);        zmq_recv(pair,&msg,0);        printf("[S]recv '%s'\n",zmq_msg_data(&msg));        zmq_msg_close(&msg);    }    for(int i=0;i<2;i++){        zmq_msg_t msg;        zmq_msg_init_size(&msg,6);        memcpy(zmq_msg_data(&msg),"world",6);        printf("[S]send '%s'\n",zmq_msg_data(&msg));        zmq_send(pair,&msg,0);        zmq_msg_close(&msg);    }    zmq_close(pair);}int main(){    void* ctx=zmq_init(2);    void* pair=zmq_socket(ctx,ZMQ_PAIR);    zmq_bind(pair,"inproc://channel");    pthread_t id;    pthread_create(&id,NULL,&second,ctx);    for(int i=0;i<2;i++){        zmq_msg_t msg;        zmq_msg_init_size(&msg,6);        memcpy(zmq_msg_data(&msg),"world",6);        printf("[M]send '%s'\n",zmq_msg_data(&msg));        zmq_send(pair,&msg,0);        zmq_msg_close(&msg);    }    for(int i=0;i<2;i++){        zmq_msg_t msg;        zmq_msg_init(&msg);        zmq_recv(pair,&msg,0);        printf("[M]recv '%s'\n",zmq_msg_data(&msg));        zmq_msg_close(&msg);    }    pthread_join(id,NULL);    zmq_close(pair);    zmq_term(ctx);    return 0;}

2.10 Publish-Subscribe

Pub-Sub模式非常简单,Pub不断地发布消息而Sub那么就不断地接收消息。因为消息的流向是单向的,所以相对于来说比较简单。subscriber可以订阅多个publisher, 多个publisher的消息会交替地到达。关于例子的话可以参考http://zguide.zeromq.org/page:all#Getting-the-Message-Out .
我们在使用的时候subscriber必须设置ZMQ_SUBSCRIBE内容,否则subscriber是接收不到数据的。对于这个内容在进行过滤的时候有用,subscriber会根据消息头进行过滤, 如果消息头不和ZMQ_SUBSCRIBE的内容匹配的话那么数据就会被丢弃。但是从现在的实现上来看的话,现在过滤过程并不是在publisher来完成的,而是在subscriber获得所有数据来进行过滤的。 如果不想进行过滤的话,那么可以将ZMQ_SUBSCRIBE内容设置为空.

zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "www.ofssln.com", 0);


2.11Missing Message
我们看下面一个例子.为了简单起见我们想让subscriber首先运行起来,然后让publisher运行起来。因为如果我们首先将publisher连接起来的话, 那么subscriber在进行连接的话就会丢失很多记录了。

//============================================================// publisher.cc,faster speed.//============================================================#include "zhelpers.h"int main(){    void* ctx=zmq_init(1);    void* pub=zmq_socket(ctx,ZMQ_PUB);    zmq_bind(pub,"tcp://*:19870");    const int header=10001;    for(int i=0;i<10;i++){        char message[20];        snprintf(message,sizeof(message),"%d %d",header,i);        printf("send '%s'\n",message);        {            zmq_msg_t msg;            zmq_msg_init_size(&msg,strlen(message)+1);            memcpy(zmq_msg_data(&msg),message,strlen(message)+1);            zmq_send(pub,&msg,0);            zmq_msg_close(&msg);            sleep(1);        }    }    zmq_close(pub);    zmq_term(ctx);    return 0;}//============================================================// subscriber.cc,litte speed.//============================================================#include "zhelpers.h"int main(){    void* ctx=zmq_init(1);    void* sub=zmq_socket(ctx,ZMQ_SUB);    zmq_setsockopt(sub,ZMQ_SUBSCRIBE,"10001",5);    zmq_connect(sub,"tcp://localhost:19870");    for(int i=0;i<10;i++){        zmq_msg_t msg;        zmq_msg_init(&msg);        zmq_recv(sub,&msg,0);        printf("recv '%s'\n",zmq_msg_data(&msg));        zmq_msg_close(&msg);        sleep(1);    }    zmq_close(sub);    zmq_term(ctx);    return 0;}

我们看到的是subscriber丢失了一条消息。这个非常好解释,那就是说虽然subsriber首先启动的话,但是只有当publisher启动之后发送了一条信息之后才能够感知到对端启动, 这个时候subscriber再进行连接,那么就造成第一条数据的丢失。(这个过程是我猜测的,但是关于这个现象在ZMQ Guide上面是有解释的)

There is one important thing to know about PUB-SUB sockets: you do not know precisely when a subscriber starts to get messages. Even if you start a subscriber, wait a while, and then start the publisher, the subscriber will always miss the first messages that the publisher sends. This is because as the subscriber connects to the publisher (something that takes a small but non-zero time), the publisher may already be sending messages out.

解决这个问题很简单,就是需要一个同步的机制。但是即使是 http://zguide.zeromq.org/page:all#Node-Coordination 这种同步机制也是不够的。robust的同步机制应该是 A more robust model could be:

  • Publisher opens PUB socket and starts sending "Hello" messages (not data).
  • Subscribers connect SUB socket and when they receive a Hello message they tell the publisher via a REQ/REP socket pair.
  • When the publisher has had all the necessary confirmations, it starts to send real data
2.12 Congestion Control
之前我们提到拥塞控制,对于PUB来说的话如果达到了HWM的话那么会直接进行丢弃。我们简单地修改一下上面的代码,让subscriber连接上但是不进行处理,而publisher不断地发送消息。

//============================================================// publisher.cc,faster speed.//============================================================#include "zhelpers.h"int main(){    void* ctx=zmq_init(1);    void* pub=zmq_socket(ctx,ZMQ_PUB);    zmq_bind(pub,"tcp://*:19870");    const int header=10001;    int i=0;    while(1){        i++;        char message[20];        snprintf(message,sizeof(message),"%d %d",header,i);        printf("send '%s'\n",message);        {            zmq_msg_t msg;            zmq_msg_init_size(&msg,strlen(message)+1);            memcpy(zmq_msg_data(&msg),message,strlen(message)+1);            zmq_send(pub,&msg,0);            zmq_msg_close(&msg);        }    }    zmq_close(pub);    zmq_term(ctx);    return 0;}//============================================================// subscriber.cc,litte speed.//============================================================#include "zhelpers.h"int main(){    void* ctx=zmq_init(1);    void* sub=zmq_socket(ctx,ZMQ_SUB);    zmq_setsockopt(sub,ZMQ_SUBSCRIBE,"10001",5);    zmq_connect(sub,"tcp://localhost:19870");    sleep(100000);    zmq_close(sub);    zmq_term(ctx);    return 0;}

然后我们看看运行之后的效果是subscriber占用的内存越来越大,而publisher的内存稳定。这是因为subscriber一旦连接上之后,那么publisher的内容就可以推送给 subscriber在sub这端进行缓存。如果一旦disconnect掉subscriber的话,因为publisher没有订阅者,那么消息直接丢弃不会在pub这端缓存。

我们可以通过设置Identity来强迫publisher进行缓存,在subscriber.cc部分加上

zmq_setsockopt(sub,ZMQ_IDENTITY,"www.ofssln.com",4);
然后启动subscriber然后挂断,因为subscriber连接上之后告诉了publisher自己的identity,那么publisher就会尝试缓存所有没有发往这个subscriber的数据。 如果没有设置PUB的HWM的话,那么PUB的内存很快就会被耗光。如果我们设置了HWM的话,那么publisher仅仅会缓存部分数据。我们还可以通过设置SWAP大小, 将部分拥塞部分结果放在磁盘上面,如果拥塞结果消息数量超过HWM的话

uint64_t hwm = 2;zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm));// Specify swap space in bytesuint64_t swap = 25000000;zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap));

2.13 Push-Pull
Push-Pull相对于Pub-Sub模式更加简单。Push-Pull模型工作方式是Divide-And-Conquer,会保证选择一个并且只有一个client来处理消息,而不像Pub-Sub一样会尝试让所有的client都获得消息。 关于例子的话可以直接参考链接http://zguide.zeromq.org/page:all#Divide-and-Conquer . 对于ZMQ的Push-Pull实现的话,server端会不断地发现新增的client连接,然后再进行消息分发的时候, 也会将这些消息分发到新增加的client上面去,使用这个功能的话就可以非常方便地处理动态添加机器的行为。

2.14
Request-Reply

我们返回来再看Example.在Example里面的话虽然server可以维护很多连接,但是读写方式是同步的,但是ZMQ是提供了异步的Request-Reply的通信模型的。 这节我们主要看看异步的Request-Reply在ZMQ里面是如何做到的。

首先ZMQ还定义了两个socket类型分别是:

  • ROUTER(XREP)
  • DEALER(XREQ)

其中ROUTER的大致功能是进行路由转发的,不要求立刻进行reply.而DEALER功能类似于PULL+PUSH,如果进行PUSH操作的话能够将消息进行负载均衡,而如果是PULL的话那么能够进行fair-queue能够均匀地将多个后端数据收集过来,然后配合REQ,REP就可以构造出很多种通信模式了。ZMQ Guide总结了一下各个socket类型特点。里面提到了Envelope会在后面说明。

我们需要深入了解Envelope的机制才能够充分利用ZMQ的灵活性。首先我们看看一个使用ROUTER/DEALER的例子 http://zguide.zeromq.org/page:all#Multithreading-with-MQ . 仔细阅读完成这个例子之后会有一个疑问,就是底层是怎么我们回复的消息应该是和哪一个链接绑定的呢?因为在worker_routine里面的话,我们只是往ZMQ_REP socket里面写信息, 这个信息最终会传回给DEALER,然后DEALER通过device交回给ROUTER,那么ROUTER需要将这个信息传回给client.所有的秘密就在于Message Envelope(信息包装).

关于Envelope可以仔细阅读这个章节 http://zguide.zeromq.org/page:all#Request-Reply-Envelopes . 但是为了方便我们理解,这里我们还是重述一遍。 从上节的介绍中我们看到了REQ/REP的Envelope就是一个empty message part.而对于DEALER来说的话没有处理任何Envelope的信息。ROUTER的Envelope是这样的:

  • 如果对端设置了identity的话,每发送一个消息的话ROUTER接收到,那么ROUTER在转发之前会在头部自动加上一个message part,内容是identity.
  • 如果对端没有设置identity的话,每发送一个消息的话ROUTER接收到,那么ROUTER在转发之前会生成一个UUID,同样自动加上一个message part,内容是UUID.

如果消息流经多个ROUTER的话,那么会自动加上多个这样的东西。不过下面的实验证明,并不是每个消息都会生成UUID的,而是针对每个连接生成UUID.

#include "zhelpers.h"int main (void) {    void *context = zmq_init (1);    void *sink = zmq_socket (context, ZMQ_ROUTER);    zmq_bind (sink, "inproc://example");    // First allow 0MQ to set the identity    void *anonymous = zmq_socket (context, ZMQ_DEALER);    zmq_connect (anonymous, "inproc://example");    s_send (anonymous, "ROUTER uses a generated UUID");    s_dump (sink);    s_send (anonymous, "ROUTER uses a generated UUID");    s_dump (sink);    zmq_close (sink);    zmq_close (anonymous);    zmq_term (context);    return 0;}

最后不管是DEALER还是REP来进行处理的话,都需要解包。只不过DEALER没有自动处理,需要我们自己在应用层解开多个message part,然后保存起来。当需要回复消息的时候, 在头部重新加上这些message part.这种方式比较灵活可以用来做异步处理。而REP逻辑就非常简单,一直解包直到第一个empty message part将其保存起来,然后当send出去的时候在头部包装, 这就解释了为什么,REP必须是一个lockstep的过程,不然的话整个逻辑就会混乱。

如果理解了ROUTER/DEALER/REQ/REP的机制之后的话,就比较容易理解如何构建一个异步客户端和服务器模型了。http://zguide.zeromq.org/page:all#Asynchronous-Client-Server .

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

篇一至此结束。后续会讲讲API,实战演示,以及我们团队在使用中遇到的一些小问题。



0 0
原创粉丝点击