网络上有很多高手分析了ZeroMQ的模式,此处没有必要再重新写,直接转过给,供大家参考,本着开源共享的精神,开始今天的学习.

-----------------------------------------------------------------------------------------------------------------------------------------

 ZMQ 提供了三个基本的通信模型,分别是“Request-Reply “,”Publisher-Subscriber“,”Parallel Pipeline”,我们从这三种模式一窥 ZMQ 的究竟

一.Request-Reply模式

  ZMQ 的 hello world!

  由 Client 发起请求,并等待 Server 回应请求。请求端发送一个简单的 hello,服务端则回应一个 world。请求端和服务端都可以是 1:N 的模型。通常把 1 认为是 Server ,N 认为是 Client 。ZMQ 可以很好的支持路由功能(实现路由功能的组件叫作 Device),把 1:N 扩展为N:M (只需要加入若干路由节点)。如图 1 所示:

zero的模式总结_学习

  图1:ZMQ 的 Request-Reply 通信

  服务端的 php 程序如下:

<?php

 =  ZMQContext (1);
 =  ZMQSocket (, ZMQ::SOCKET_REP);
-&gt;bind ("tcp://*:5555");
() {
 = -&gt;recv ();
 ("Received request: [%s]\n", );
 
 (1);
 
-&gt;send ("World");
}

  Client 程序如下:

<?php
 

 =  ZMQContext ();
 
 
 "Connecting to hello world server...\n";
 
 =  ZMQSocket (, ZMQ::SOCKET_REQ);
 
-&gt;connect ("tcp://localhost:5555");
 
( = 0;  != 10; ++) {
 
     ("Sending request %d...\n", );
 
    -&gt;send ("Hello");
 
     = -&gt;recv ();
 
     ("Received reply %d: [%s]\n", , );
 
}

  从以上的过程,我们可以了解到使用 ZMQ 写基本的程序的方法,需要注意的是:

  a) 服务端和客户端无论谁先启动,效果是相同的,这点不同于 Socket。

  b) 在服务端收到信息以前,程序是阻塞的,会一直等待客户端连接上来。

  c) 服务端收到信息以后,会 send 一个“World”给客户端。值得注意的是一定是 client 连接上来以后,send 消息给 Server,然后 Server 再 rev 然后响应 client,这种一问一答式的。如果 Server 先 send,client 先 rev 是会报错的。

  d) ZMQ 通信通信单元是消息,他除了知道 Bytes 的大小,他并不关心的消息格式。因此,你可以使用任何你觉得好用的数据格式。Xml、Protocol Buffers、Thrift、json 等等。

  e) 虽然可以使用 ZMQ 实现 HTTP 协议,但是,这绝不是他所擅长的。

  二 ZMQ 的 Publish-subscribe 模式

  我们可以想象一下天气预报的订阅模式,由一个节点提供信息源,由其他的节点,接受信息源的信息,如图 2 所示:

zero的模式总结_使用_02

  图2:ZMQ 的 Publish-subscribe

  示例代码如下 :

  Publisher:

<?php

 
 =  ZMQContext ();
 = -&gt;getSocket (ZMQ::SOCKET_PUB);
-&gt;bind ("tcp://*:5556");
 
 () {
 = (0, 100000);
 = (-80, 135);
 = (10, 60);
 
 =  ("%05d %d %d", , , );
-&gt;send ();
}</pre>
Subscriber
<pre>&lt;?php

 
 =  ZMQContext ();
 
 "Collecting updates from weather server…", ;
 =  ZMQSocket (, ZMQ::SOCKET_SUB);
-&gt;connect ("tcp://localhost:5556");
 
 = ['argc'] &gt; 1 ? ['argv'][1] : "10001";
-&gt;setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE, );
 
 = 0;
 ( = 0;  &lt; 100; ++) {
 = -&gt;recv ();
 (, "%d %d %d", , , );
 += ;
}
 ("Average temperature for zipcode '%s' was %dF\n",
, (int) ( / ));

  这段代码讲的是,服务器端生成随机数 zipcode、temperature、relhumidity 分别代表城市代码、温度值和湿度值。然后不断的广播信息,而客户端通过设置过滤参数,接受特定城市代码的信息,收集完了以后,做一个平均值。

  a) 与 Hello World 不同的是,Socket 的类型变成 SOCKET_PUB 和 SOCKET_SUB 类型。

  b) 客户端需要$subscriber->setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE, $filter);设置一个过滤值,相当于设定一个订阅频道,否则什么信息也收不到。

  c) 服务器端一直不断的广播中,如果中途有 Subscriber 端退出,并不影响他继续的广播,当 Subscriber 再连接上来的时候,收到的就是后来发送的新的信息了。这对比较晚加入的,或者是中途离开的订阅者,必然会丢失掉一部分信息,这是这个模式的一个问题,所谓的 Slow joiner。稍后,会解决这个问题。

  d) 但是,如果 Publisher 中途离开,所有的 Subscriber 会 hold 住,等待 Publisher 再上线的时候,会继续接受信息。

三 ZMQ 的 PipeLine 模型

  想象一下这样的场景,如果需要统计各个机器的日志,我们需要将统计任务分发到各个节点机器上,最后收集统计结果,做一个汇总。PipeLine 比较适合于这种场景,他的结构图,如图 3 所示。

zero的模式总结_安装_03

  图3:ZMQ 的 PipeLine 模型

  Parallel task ventilator in PHP

<?php

 
 =  ZMQContext ();
 
 =  ZMQSocket (, ZMQ::SOCKET_PUSH);
-&gt;bind ("tcp://*:5557");
 
 "Press Enter when the workers are ready: ";
 = ('php://stdin', 'r');
 = (, 512);
();
 "Sending tasks to workers…", ;
 
-&gt;send (0);
 
 = 0;  ( = 0;  &lt; 100; ++) {
 = (1, 100);
 += ;
-&gt;send ();
 
}
 ("Total expected cost: %d msec\n", );
 (1);

  Parallel task worker in PHP

<?php

 
 =  ZMQContext ();
 
 =  ZMQSocket (, ZMQ::SOCKET_PULL);
-&gt;connect ("tcp://localhost:5557");
 
 =  ZMQSocket (, ZMQ::SOCKET_PUSH);
-&gt;connect ("tcp://localhost:5558");
 
 () {
 = -&gt;recv ();
 
 , ;
 
( * 1000);
 
-&gt;send ("");
}

  Parallel task sink in PHP

<?php

 
 =  ZMQContext ();
 =  ZMQSocket (, ZMQ::SOCKET_PULL);
-&gt;bind ("tcp://*:5558");
 
 = -&gt;recv ();
 
 = ();
 
 = 0;  ( = 0;  &lt; 100; ++) {
 = -&gt;recv ();
( % 10 == 0) {
 ":";
}  {
 ".";
}
}
 
 = ();
 
 = ( - ) * 1000;
 ;
 ("Total elapsed time: %d msec", );
 ;

  从程序中,我们可以看到,task ventilator 使用的是 SOCKET_PUSH,将任务分发到 Worker 节点上。而 Worker 节点上,使用 SOCKET_PULL 从上游接受任务,并使用 SOCKET_PUSH 将结果汇集到 Slink。值得注意的是,任务的分发的时候也同样有一个负载均衡的路由功能,worker 可以随时自由加入,task ventilator 可以均衡将任务分发出去。

 四 其他扩展模式

  通常,一个节点,即可以作为 Server,同时也能作为 Client,通过 PipeLine 模型中的 Worker,他向上连接着任务分发,向下连接着结果搜集的 Sink 机器。因此,我们可以借助这种特性,丰富的扩展原有的三种模式。例如,一个代理 Publisher,作为一个内网的 Subscriber 接受信息,同时将信息,转发到外网,其结构图如图 4 所示。

zero的模式总结_ZeroMQ_04

  图4:ZMQ 的扩展模式

五 多个服务器

  ZMQ 和 Socket 的区别在于,前者支持N:M的连接,而后者则只是1:1的连接,那么一个 Client 连接多个 Server 的情况是怎样的呢,我们通过图 5 来说明。

zero的模式总结_ZeroMQ_05

  图5:ZMQ 的N:1的连接情况

  我们假设 Client 有 R1,R2,R3,R4四个任务,我们只需要一个 ZMQ 的 Socket,就可以连接四个服务,他能够自动均衡的分配任务。如图 5 所示,R1,R4自动分配到了节点A,R2到了B,R3到了C。如果我们是N:M的情况呢?这个扩展起来,也不难,如图 6 所示。

zero的模式总结_ZeroMQ_06

  图6:N:M的连接

  我们通过一个中间结点(Broker)来进行负载均衡的功能。我们通过代码了解,其中的 Client 和我们的 Hello World 的 Client 端是一样的,而 Server 端的不同是,他不需要监听端口,而是需要连接 Broker 的端口,接受需要处理的信息。所以,我们重点阅读 Broker 的代码:

<?php

 
 =  ZMQContext ();
 =  ZMQSocket (, ZMQ::SOCKET_ROUTER);
 =  ZMQSocket (, ZMQ::SOCKET_DEALER);
-&gt;bind ("tcp://*:5559");
-&gt;bind ("tcp://*:5560");
 
 =  ZMQPoll ();
-&gt;add (, ZMQ::POLL_IN);
-&gt;add (, ZMQ::POLL_IN);
 =  = ();
 
() {
 = -&gt;poll (, );
 
(  ) {
( === ) {
() {
 = -&gt;recv ();
 = -&gt;getSockOpt (ZMQ::SOCKOPT_RCVMORE);
-&gt;send (,  ? ZMQ::MODE_SNDMORE : );
(!) {
; }
}
}
 ( === ) {
 = -&gt;recv ();
 = -&gt;getSockOpt (ZMQ::SOCKOPT_RCVMORE);
-&gt;send (,  ? ZMQ::MODE_SNDMORE : );
(!) {
; }
}
}
}

  Broker 监听了两个端口,接受从多个 Client 端发送过来的数据,并将数据,转发给 Server。在 Broker 中,我们监听了两个端口,使用了两个 Socket,那么对于多个 Socket 的情况,我们是不需要通过轮询的方式去处理数据的,在之前,我们可以使用 libevent 实现,异步的信息处理和传输。而现在,我们只需要使用 ZMQ 的$poll->poll 以实现多个 Socket 的异步处理。

 六 进程间的通信

  ZMQ 不仅能通过 TCP 完成节点间的通信,也可以通过 Socket 文件完成进程间的通信。如图 7 所示,我们 fork 三个 PHP 进程,将进程 1 的数据,通过 Socket 文件发送到进程3。

zero的模式总结_安装_07

图7:进程间的通信

<?php
 
 step1() {
 
         =  ZMQContext ();
 
         
         =  ZMQSocket (, ZMQ::SOCKET_PAIR);
 
        -&gt;connect ("ipc://step2.ipc");
 
        -&gt;send ("hello ,i am step1");
 
}
 
 step2() {
 
         = pcntl_fork ();
 
        ( == 0) {
 
                step1();
 
                ();
 
        }
 
         =  ZMQContext ();
 
         
         =  ZMQSocket (, ZMQ::SOCKET_PAIR);
 
        -&gt;bind ("ipc://step2.ipc");
 
         
        (10);
 
         = -&gt;recv ();
 
         "step2 receiver is ". ;
 
        (10);
 
         
         =  ZMQSocket (, ZMQ::SOCKET_PAIR);
 
        -&gt;connect ("ipc://step3.ipc");
 
        -&gt;send ();
 
}
 
 
 = pcntl_fork ();
 
( == 0) {
 
        step2();
 
        ();
 
}
 
 =  ZMQContext ();
 
 =  ZMQSocket (, ZMQ::SOCKET_PAIR);
 
-&gt;bind ("ipc://step3.ipc");
 
 
 = -&gt;recv ();
 
 "the result is {}".;

  在运行中,我们可以看到多了两个文件,如图 8 所示。

zero的模式总结_ZeroMQ_08

  图8:运行过程中生成的文件

 七 利用 ZeroMQ 实现一个配置推送中心

  当我们将 WEB 代码部署到集群上的时候,如果需要实时的将最新的配置信息,主动的推送到各个机器节点。在此过程中,我们一定要保证,各个节点收到的信息的一致性和正确性,如果使用 HTTP,由于他的无状态性,我们无法保证信息的一致性,当然,你可以使用 HTTP 来实现,只是更复杂,为什么不用 ZMQ?他能让你更简单的实现这些功能。

  我们使用 ZMQ 的信息订阅模式。在那个模式中,我们注意到,对于后来的加入节点,始终会丢失在他加入之前,已经发送的信息(Slow joiner)。我们可以开启另外一个 ZMQ 的通信通道,用于报告当前节点的情况(节点的身份、准备状态等),其结构如图 9 所示。

zero的模式总结_安装_09

  图9:扩展 ZMQ 的订阅者模式

  我们通过$context->getSocket (ZMQ::SOCKET_REQ);设置一个新的 Request-Reply 连接,来用于 Subscriber 向 Publisher 报告自己的身份信息,而 Publisher 则等待所有的 Subscriber 都连接上的时候,再选择 Publish 自己的信息。

  Subscriber 端的程序如下:

<?php
 
 = ['argc'] &gt; 1 ? ['argv'][1] : "s1";
 
 =  ZMQContext (2);
 
 =  ZMQSocket (,ZMQ::SOCKET_SUB);
 
-&gt;connect ("tcp://localhost:5561");
 
 
-&gt;setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE,"");
 
 = -&gt;getSocket (ZMQ::SOCKET_REQ);
 
-&gt;connect ("tcp://localhost:5562");
 
(1) {
 
 
-&gt;send ();
 
 = -&gt;recv ();
 
 ."\r\n";
 
 (!()) {
 
 = -&gt;recv ();
 
 = json_decode ();
 
();
 
}
 
}

  Publisher 端的程序如下:

<?php
 
["TAOKE_BTS"]["ENABLE"] = ;
 
["QP_BTS"]["ENABLE"] = ;
 
["QP_BTS"]["TK_TEST"] = 13;
 
 = json_encode ();
 
 = ("s2","s1","s3");
 
 =  ZMQContext (10);
 
 
 =  ZMQSocket (,ZMQ::SOCKET_PUB);
 
-&gt;bind ("tcp://*:5561");
 
 
 =  ZMQSocket (,ZMQ::SOCKET_REP);
 
-&gt;bind ("tcp://*:5562");
 
(()!=0) {
 
       = -&gt;recv ();
 
         "{} is connect!\r\n";
 
 ((, )) {  
         = (, );
 
        ([]);
 
         " has come in!\r\n";
 
        -&gt;send ("Version is 2.0");
 
}  {
 
        -&gt;send ("You are a stranger!");
 
}
 
}
 
-&gt;send ();
 
?&gt;

  每个节点通过 5562 端口,使用 Rep 模式和 Publisher 连接,通过这个连接告之 Publisher 自己的机器名,而 Publisher 端通过白名单的方式,维护一个机器列表,当机器列表中所有的机器连接上来以后,通过 5561 端口,将最新的配置信息发送出去。

  后续的处理,Subscriber 可以选择将配置信息写入到 APC 缓存,程序将始终从缓存中读取部分配置信息,Subscriber 并将更新后的状态信息,实时的通过 5562 报告给 Publisher。

  虽然,在本示例中不会出现,但是,如果需要发布的信息量过大,在接受信息的过程中,Subscriber 端突然中断网络(或者是程序崩溃),那么当他在连接上来的时候,有部分信息就会丢失?ZMQ 考虑到这个问题,通过$subscriber->setSockOpt (ZMQ::SOCKOPT_IDENTITY, $hostname);设置一个 id,当这个 id 的 Subscriber 重新连接上来的时候,他可以从上次中断的地方,继续接受信息,当然,节点的中断,不会影响其他的节点继续的接受信息。

  那么 ZMQ 是怎么实现断线重连后,继续发送信息呢 ?他会将断开的 Subscriber 应该接受到的信息发到内存中,等待他重新上线后,将缓存的信息,继续发送给他。当然,内存必然是有限的,过多就会出现内存溢出。ZMQ 通过

  SetSockOpt (ZMQ::SOCKOPT_SWAP, 250000)设置 Swap 空间的大小,来防止 out of memory and crash。最终,我们的程序运行结果,如图 10 所示。

zero的模式总结_ZeroMQ_10

  图 10:配置中心的运行结果

  当然,这只是一个大体的思路,如果应用到实际的成产环境中,还需要考虑更多的问题,包含稳定性,容错等等。然而,ZMQ 由于高并发,以及稳定性和易用性,前景不错,他的目标是进入 Linux 内核,我们期待那一天的到来。