zeromq简介及各个通讯模式实例详解(附java实现)

一、Zeromq简介

ZeroMQ是一种基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。ZeroMQ是网络通信中新的一层,介于应用层和传输层之间(按照TCP/IP划分),其是一个可伸缩层,可并行运行,分散在分布式系统间。

zeroMQ在设计上主要采用了以下几个高性能的特征:

1、无锁的队列模型

  对于跨线程间的交互(用户端和session)之间的数据交换通道pipe,采用无锁的队列算法CAS;在pipe的两端注册有异步事件,在读或者写消息到pipe的时,会自动触发读写事件。

2、批量处理的算法

 对于传统的消息处理,每个消息在发送和接收的时候,都需要系统的调用,这样对于大量的消息,系统的开销比较大,zeroMQ对于批量的消息,进行了适应性的优化,可以批量的接收和发送消息。

3、多核下的线程绑定,无须CPU切换

  区别于传统的多线程并发模式,信号量或者临界区, zeroMQ充分利用多核的优势,每个核绑定运行一个工作者线程,避免多线程之间的CPU切换开销。


ZMQ连接和传统的TCP连接是有区别的,主要有:

  • 使用多种协议,inproc(进程内)、ipc(进程间)、tcp、pgm(广播)、epgm;
  • 当客户端使用zmq_connect()时连接就已经建立了,并不要求该端点已有某个服务使用zmq_bind()进行了绑定;
  • 连接是异步的,并由一组消息队列做缓冲;
  • 连接会表现出某种消息模式,这是由创建连接的套接字类型决定的;
  • 一个套接字可以有多个输入和输出连接;
  • ZMQ没有提供类似zmq_accept()的函数,因为当套接字绑定至端点时它就自动开始接受连接了;
  • 应用程序无法直接和这些连接打交道,因为它们是被封装在ZMQ底层的。

ZMQ提供了一组单播传输协议(inporc, ipc, tcp),和两个广播协议(epgm, pgm)。广播协议是比较高级的协议,我们会在以后讲述。如果你不能回答我扇出比例会影响一对多的单播传输时,就先不要去学习广播协议了吧。

一般而言我们会使用tcp作为传输协议,这种TCP连接是可以脱机运作的,它灵活、便携、且足够快速。为什么称之为脱机,是因为ZMQ中的TCP连接不需要该端点已经有某个服务进行了绑定,客户端和服务端可以随时进行连接和绑定,这对应用程序而言都是透明的。

进程间协议,即ipc,和tcp的行为差不多,但已从网络传输中抽象出来,不需要指定IP地址或者域名。这种协议很多时候会很方便,本指南中的很多示例都会使用这种协议。ZMQ中的ipc协议同样可以是脱机的,但有一个缺点——无法在Windows操作系统上运作,这一点也许会在未来的ZMQ版本中修复。我们一般会在端点名称的末尾附上.ipc的扩展名,在UNIX系统上,使用ipc协议还需要注意权限问题。你还需要保证所有的程序都能够找到这个ipc端点。

进程内协议,即inproc,可以在同一个进程的不同线程之间进行消息传输,它比ipc或tcp要快得多。这种协议有一个要求,必须先绑定到端点,才能建立连接,也许未来也会修复。通常的做法是先启动服务端线程,绑定至端点,后启动客户端线程,连接至端点。

TCP套接字和ZMQ套接字之间在传输数据方面的区别:

  • ZMQ套接字传输的是消息,而不是字节(TCP)或帧(UDP)。消息指的是一段指定长度的二进制数据块,我们下文会讲到消息,这种设计是为了性能优化而考虑的,所以可能会比较难以理解。
  • ZMQ套接字在后台进行I/O操作,也就是说无论是接收还是发送消息,它都会先传送到一个本地的缓冲队列,这个内存队列的大小是可以配置的。
  • ZMQ套接字可以和多个套接字进行连接(如果套接字类型允许的话)。TCP协议只能进行点对点的连接,而ZMQ则可以进行一对多(类似于无线广播)、多对多(类似于邮局)、多对一(类似于信箱),当然也包括一对一的情况。
  • ZMQ套接字可以发送消息给多个端点(扇出模型),或从多个端点中接收消息(扇入模型)

二、Zeromq模式详解

1.Request-Reply模式

问答模式,由请求端发起请求,然后等待回应端应答。一个请求必须对应一个回应,从请求端的角度来看是发-收配对,从回应端的角度是收-发对。请求端可以是1~N个。该模型主要用于远程调用及任务分配等。

使用REQ-REP套接字发送和接受消息是需要遵循一定规律的。客户端首先使用zmq_send()发送消息,再用zmq_recv()接收,如此循环。如果打乱了这个顺序(如连续发送两次)则会报错。类似地,服务端必须先进行接收,后进行发送。

也就是说Request-Reply模式是严格同步的,Request端必须先发送后接受,reply端必须先接受后发送。

深入到信封通信原理,Request在发送数据帧之前一并包含了一个空白的数据分割符数据帧,即在程序中虽然只是发送了一个数据帧作为参数,实际Request套接字又在数据帧的基础上添加了空字符数据帧。Request在接受的时候会去掉空白分隔符数据帧,直接将实际的数据返回到应用程序。reply套接字在接受的时候会读取消息帧并存储一直遇到空白分隔符,然后将剩余的消息返回到应用程序,在发送的时候会将存储的消息与待发送的数据一并发送出去。

zeromq简介及各个通讯模式实例详解(附java实现)_第1张图片

Server:

public static void main(String[] args) throws Exception {
        ZMQ.Context context = ZMQ.context(1);
        // Socket to talk to clients
        ZMQ.Socket responder = context.socket(ZMQ.REP);
        responder.bind("tcp://*:5555");
        while (!Thread.currentThread().isInterrupted()) {
            // Wait for next request from the client
            byte[] request = responder.recv(0);
            System.out.println("Received Hello");
            // Do some 'work'
            Thread.sleep(1000);
            // Send reply back to client
            String reply = "World";
            responder.send(reply.getBytes(), 0);
        }
        responder.close();
        context.term();
    }

Client:

 public static void main(String[] args) {
       ZMQ.Context context = ZMQ.context(1);
       //  Socket to talk to server
       System.out.println("Connecting to hello world server…");
       ZMQ.Socket requester = context.socket(ZMQ.REQ);
       requester.connect("tcp://localhost:5555");
       for (int requestNbr = 0; requestNbr != 100; requestNbr++) {
           String request = "Hello";
           System.out.println("Sending Hello " + requestNbr);
           requester.send(request.getBytes(), 0);
           byte[] reply = requester.recv(0);
           System.out.println("Received " + new String(reply) + " " + requestNbr);
       }
       requester.close();
       context.term();
   }

eclipse下maven项目工程下载地址,可直接运行:点击打开链接

2.Pub-Sub模式

发布订阅模式 发布端单向分发数据,且不关心是否把全部信息发送给订阅端。如果发布端开始发布信息时,订阅端尚未连接上来,则这些信息会被直接丢弃。订阅端未连接导致信息丢失的问题,可以通过与请求回应模型组合来解决。订阅端只负责接收,而不能反馈,且在订阅端消费速度慢于发布端的情况下,会在订阅端堆积数据。该模型主要用于数据分发。天气预报、微博明星粉丝可以应用这种经典模型。

在使用SUB套接字时,必须使用subscribe()方法来设置订阅的内容。如果你不设置订阅内容,那将什么消息都收不到。订阅信息可以是任何字符串,可以设置多次。只要消息满足其中一条订阅信息,SUB套接字就会收到。


PUB-SUB套接字组合是异步的。客户端在一个循环体中使用zmq_recv()接收消息,如果向SUB套接字发送消息则会报错;类似地,服务端可以不断地使用zmq_send()发送消息,但不能在PUB套接字上使用zmq_recv()。

关于PUB-SUB套接字,还有一点需要注意:你无法得知SUB是何时开始接收消息的。就算你先打开了SUB套接字,后打开PUB发送消息,这时SUB还是会丢失一些消息的,因为建立连接是需要一些时间的。很少,但并不是零。

我们知道在建立TCP连接时需要进行三次握手,会耗费几毫秒的时间,而当节点数增加时这个数字也会上升。在这么短的时间里,ZMQ就可以发送很多很多消息了。举例来说,如果建立连接需要耗时5毫秒,而ZMQ只需要1毫秒就可以发送完这1000条消息。

所以需要发布者和订阅者同步,只有当订阅者准备好时发布者才会开始发送消息。有一种简单的方法来同步PUB和SUB,就是让PUB延迟一段时间再发送消息。现实编程中我不建议使用这种方式,因为它太脆弱了,而且不好控制。不过这里我们先暂且使用sleep的方式来解决。

另一种同步的方式则是认为发布者的消息流是无穷无尽的,因此丢失了前面一部分信息也没有关系。我们的气象信息客户端就是这么做的。

示例中的气象信息客户端会收集指定邮编的一千条信息,其间大约有1000万条信息被发布。你可以先打开客户端,再打开服务端,工作一段时间后重启服务端,这时客户端仍会正常工作。当客户端收集完所需信息后,会计算并输出平均温度。

关于发布-订阅模式的几点说明:

  • 订阅者可以连接多个发布者,轮流接收消息;
  • 如果发布者没有订阅者与之相连,那它发送的消息将直接被丢弃;
  • 如果你使用TCP协议,那当订阅者处理速度过慢时,消息会在发布者处堆积。
  • 在目前版本的ZMQ中,消息的过滤是在订阅者处进行的。也就是说,发布者会向订阅者发送所有的消息,订阅者会将未订阅的消息丢弃。
  • zeromq简介及各个通讯模式实例详解(附java实现)_第2张图片
Server:

public static void main (String[] args) throws Exception {
       //  Prepare our context and publisher
       ZMQ.Context context = ZMQ.context(1);
       ZMQ.Socket publisher = context.socket(ZMQ.PUB);
       publisher.bind("tcp://*:5556");

       //  Initialize random number generator
       Random srandom = new Random(System.currentTimeMillis());
       while (!Thread.currentThread ().isInterrupted ()) {
           //  Get values that will fool the boss
           int zipcode, temperature, relhumidity;
           zipcode = 10000 + srandom.nextInt(10000) ;
           temperature = srandom.nextInt(215) - 80 + 1;
           relhumidity = srandom.nextInt(50) + 10 + 1;

           //  Send message to all subscribers
           String update = String.format("%05d %d %d", zipcode, temperature, relhumidity);
           publisher.send(update, 0);
       }
       publisher.close ();
       context.term ();
   }

Client1:

 public static void main (String[] args) throws IOException {
       ZMQ.Context context = ZMQ.context(1);
       //  Socket to talk to server
       System.out.println("Collecting updates from weather server");
       ZMQ.Socket subscriber = context.socket(ZMQ.SUB);
       subscriber.connect("tcp://localhost:5556");

       //  Subscribe to zipcode, default is NYC, 10001
       String filter = (args.length > 0) ? args[0] : "10001 ";
       subscriber.subscribe(filter.getBytes());

       //  Process 100 updates
       int update_nbr;
       long total_temp = 0;
       for (update_nbr = 0; update_nbr < 100; update_nbr++) {
           //  Use trim to remove the tailing '0' character
           String string = subscriber.recvStr(0).trim();
           StringTokenizer sscanf = new StringTokenizer(string, " ");
           int zipcode = Integer.valueOf(sscanf.nextToken());
           int temperature = Integer.valueOf(sscanf.nextToken());
           int relhumidity = Integer.valueOf(sscanf.nextToken());

           total_temp += temperature;
       }
       System.out.println("Average temperature for zipcode '"
               + filter + "' was " + (int) (total_temp / update_nbr));
       System.out.println("...");
       System.in.read();
       subscriber.close();
       context.term();
   }

Client2;

 public static void main (String[] args) throws IOException {
       ZMQ.Context context = ZMQ.context(1);
       //  Socket to talk to server
       System.out.println("Collecting updates from weather server");
       ZMQ.Socket subscriber = context.socket(ZMQ.SUB);
       subscriber.connect("tcp://localhost:5556");

       //  Subscribe to zipcode, default is NYC, 10001
       String filter = (args.length > 0) ? args[0] : "10002";
       subscriber.subscribe(filter.getBytes());

       //  Process 100 updates
       int update_nbr;
       long total_temp = 0;
       for (update_nbr = 0; update_nbr < 100; update_nbr++) {
           //  Use trim to remove the tailing '0' character
           String string = subscriber.recvStr(0).trim();

           StringTokenizer sscanf = new StringTokenizer(string, " ");
           int zipcode = Integer.valueOf(sscanf.nextToken());
           int temperature = Integer.valueOf(sscanf.nextToken());
           int relhumidity = Integer.valueOf(sscanf.nextToken());

           total_temp += temperature;
       }
       System.out.println("Average temperature for zipcode '"
               + filter + "' was " + (int) (total_temp / update_nbr));
       System.out.println("...");
       System.in.read();   
       subscriber.close();
       context.term();
   }

Eclipse下maven项目工程下载地址,可直接运行:点击打开链接

3.Push-Poll模式

Server端作为Push端,而Client端作为Pull端,如果有多个Client端同时连接到Server端,则Server端会在内部做一个负载均衡,采用平均分配的算法,将所有消息均衡发布到Client端上。如果有多个Server端同时连接到Client端,这里Push与Pull之间的对应关系是多个Push角色对应一个Pull角色,在ZeroMQ中,给这种结构取的名叫做公平队列,这里也就是说将Pull角色理解为一个队列,各个Push角色不断的向这个队列中发送数据。与发布订阅模型相比,推拉模型在没有消费者的情况下,发布的消息不会被消耗掉;在消费者能力不够的情况下,能够提供多消费者并行消费解决方案。该模型主要用于多任务并行处理。

下面一个示例程序中,我们将使用ZMQ进行超级计算,也就是并行处理模型:

  • 任务分发器会生成大量可以并行计算的任务;
  • 有一组worker会处理这些任务;
  • 结果收集器会在末端接收所有worker的处理结果,进行汇总。

并行处理模型的几个细节:

  • worker上游和任务分发器相连,下游和结果收集器相连,这就意味着你可以开启任意多个worker。但若worker是绑定至端点的,而非连接至端点,那我们就需要准备更多的端点,并配置任务分发器和结果收集器。所以说,任务分发器和结果收集器是这个网络结构中较为稳定的部分,因此应该由它们绑定至端点,而非worker,因为它们较为动态。

  • 我们需要做一些同步的工作,等待worker全部启动之后再分发任务。这点在ZMQ中很重要,且不易解决。连接套接字的动作会耗费一定的时间,因此当第一个worker连接成功时,它会一下收到很多任务。所以说,如果我们不进行同步,那这些任务根本就不会被并行地执行。你可以自己试验一下。

  • 任务分发器使用PUSH套接字向worker均匀地分发任务(假设所有的worker都已经连接上了),这种机制称为负载均衡,以后我们会见得更多。

  • 结果收集器的PULL套接字会均匀地从worker处收集消息,这种机制称为公平队列。

zeromq简介及各个通讯模式实例详解(附java实现)_第3张图片

Eclipse下maven项目工程下载地址,可直接运行:点击打开链接

三、Zeromq进阶

1.多套接字处理

如何读取多个套接字中的消息呢?最简单的方法是将套接字连接到多个端点上,让ZMQ使用公平队列的机制来接受消息。如果不同端点上的套接字类型是一致的,那可以使用这种方法。但是,如果一个套接字的类型是PULL,另一个是PUB怎么办?如果现在开始混用套接字类型,那将来就没有可靠性可言了。

正确的方法应该是使用zmq_poll()函数。zmq_poll()函数为应用程序提供了一种对一组socket进行多路I/O事件水平触发的机制。

Eclipse下maven项目工程下载地址,可直接运行:点击打开链接

2.代理服务

发布-订阅代理

我们经常会需要将发布-订阅模式扩充到不同类型的网络中。比如说,有一组订阅者是在外网上的,我们想用广播的方式发布消息给内网的订阅者,而用TCP协议发送给外网订阅者。

我们要做的就是写一个简单的代理服务装置,在发布者和外网订阅者之间搭起桥梁。这个装置有两个端点,一端连接内网上的发布者,另一端连接到外网上。它会从发布者处接收订阅的消息,并转发给外网上的订阅者们。

我们称这个装置为代理,因为它既是订阅者,又是发布者。这就意味着,添加该装置时不需要更改其他程序的代码,只需让外网订阅者知道新的网络地址即可。


代理服务:

public static void main (String[] args) {
//Prepare our context and sockets
Context context = ZMQ.context(1);

//This is where the weather server sits
Socket frontend =context.socket(ZMQ.SUB);
frontend.connect("tcp://192.168.55.210:5556");

//This is our public endpoint for subscribers
Socket backend= context.socket(ZMQ.PUB);
backend.bind("tcp://10.1.1.0:8100");

//Subscribe on everything
frontend.subscribe("".getBytes());

//Run the proxy until the user interrupts us
ZMQ.proxy (frontend, backend, null);

frontend.close();
backend.close();
context.term();
}

请求-应答代理

在请求应答模式实例中是一个客户端和一个服务端进行通信。但在真实环境中,我们会需要让多个客户端和多个服务端进行通信。关键问题在于,服务端应该是无状态的,所有的状态都应该包含在一次请求中,或者存放其它介质中,如数据库。

下面就让我们编写这样一个组件。这个代理会绑定到两个端点,前端端点供客户端连接,后端端点供服务端连接。它会使用zmq_poll()来轮询这两个套接字,接收消息并进行转发。装置中不会有队列的存在,因为ZMQ已经自动在套接字中完成了。

在使用REQ和REP套接字时,其请求-应答的会话是严格同步。客户端发送请求,服务端接收请求并发送应答,由客户端接收。如果客户端或服务端中的一个发生问题(如连续两次发送请求),程序就会报错。

但是,我们的代理装置必须要是非阻塞式的,虽然可以使用zmq_poll()同时处理两个套接字,但这里显然不能使用REP和REQ套接字。

幸运的是,我们有DEALER和ROUTER套接字可以胜任这项工作,进行非阻塞的消息收发。DEALER过去被称为XREQ,ROUTER被称为XREP,但新的代码中应尽量使用DEALER/ROUTER这种名称。

使用请求-应答代理可以让你的C/S网络结构更易于扩展:客户端不知道服务端的存在,服务端不知道客户端的存在。网络中唯一稳定的组件是中间的代理装置。所以客户端发送的request不会由特定的reply处理。

下方的简图描述了一个请求-应答模式,REQ和ROUTER通信,DEALER再和REP通信。ROUTER和DEALER之间我们则需要进行消息转发:

zeromq简介及各个通讯模式实例详解(附java实现)_第4张图片

Eclipse下maven项目工程下载地址,可直接运行:点击打开链接


3.内置服务

ZMQ提供了一些内置的装置,不过大多数人需要自己手动编写这些装置。内置装置有:

  • QUEUE,可用作请求-应答代理;
  • FORWARDER,可用作发布-订阅代理服务;
  • STREAMER,可用作管道模式代理。

可以使用zmq_device()来启动一个装置,需要传递两个套接字给它:

zmq_device (ZMQ_QUEUE, frontend, backend);

java中的使用方法   ZMQ.proxy (frontend, backend, null);

启动了QUEUE队列就如同在网络中加入了一个请求-应答代理,只需为其创建已绑定或连接的套接字即可。

内置装置会恰当地处理错误,而我们手工实现的代理并没有加入错误处理机制。所以说,当你能够在程序中使用内置装置的时候就尽量用吧。

可能你会像某些ZMQ开发者一样提出这样一个问题:如果我将其他类型的套接字传入这些装置中会发生什么?答案是:别这么做。你可以随意传入不同类型的套接字,但是执行结果会非常奇怪。所以,QUEUE装置应使用ROUTER/DEALER套接字、FORWARDER应使用SUB/PUB、STREAMER应使用PULL/PUSH。

前面的实例中有关的parallel-pipeline并行处理、以及代理服务都可以使用内置服务完成,减少编码工作量。

比如请求-问答代理可以简化成以下写法:

public static void main (String[] args) {
       //  Prepare our context and sockets
       Context context = ZMQ.context(1);
       //  Socket facing clients
       Socket frontend = context.socket(ZMQ.ROUTER);
       frontend.bind("tcp://*:5559");
       //  Socket facing services
       Socket backend = context.socket(ZMQ.DEALER);
       backend.bind("tcp://*:5560");

       //  Start the proxy
       ZMQ.proxy (frontend, backend, null);

       //  We never get here but clean up anyhow
       frontend.close();
       backend.close();
       context.term();
   }

当你需要其他的套接字类型进行组合时,那就需要自己编写装置了。

4.多线程

使用ZMQ进行多线程编程时,不需要考虑互斥、锁、或其他并发程序中要考虑的因素,你唯一要关心的仅仅是线程之间的消息

如何用ZMQ进行多线程编程,以下是一些规则:

  • 不要在不同的线程之间访问同一份数据,如果要用到传统编程中的互斥机制,那就有违ZMQ的思想了。唯一的例外是ZMQ上下文对象,它是线程安全的。

  • 必须为进程创建ZMQ上下文,并将其传递给所有你需要使用inproc协议进行通信的线程;

  • 你可以将线程作为单独的任务来对待,使用自己的上下文,但是这些线程之间就不能使用inproc协议进行通信了。这样做的好处是可以在日后方便地将程序拆分为不同的进程来运行。

  • 不要在不同的线程之间传递套接字对象,这些对象不是线程安全的。从技术上来说,你是可以这样做的,但是会用到互斥和锁的机制,这会让你的应用程序变得缓慢和脆弱。唯一合理的情形是,在某些语言的ZMQ类库内部,需要使用垃圾回收机制,这时可能会进行套接字对象的传递。

下面我们举一个实例,让原来的Hello World服务变得更为强大。原来的服务是单线程的,如果请求较少,自然没有问题。ZMQ的线程可以在一个核心上高速地运行,执行大量的工作。但是,如果有一万次请求同时发送过来会怎么样?因此,现实环境中,我们会启动多个worker线程,他们会尽可能地接收客户端请求,处理并返回应答。

server:

public class Worker extends Thread {
private Context context;
private int workerNum;


    Worker (Context context,int worker)
    {
        this.context = context;
        this.workerNum=worker;
    }
    @Override
    public void run() {
        ZMQ.Socket socket = context.socket(ZMQ.REP);
        socket.connect ("inproc://workers");

        while (true) {

            //  Wait for next request from client (C string)
            String request = socket.recvStr (0);
            System.out.println ( Thread.currentThread().getName() + " Received request: [" + request + "]");

            //  Do some 'work'
            try {
                Thread.sleep (1000);
            } catch (InterruptedException e) {
            }

            //  Send reply back to client (C string)
            socket.send("work"+ this.workerNum +"reply is: "+"world", 0);
        }
    }
}


 public static void main (String[] args) {


       Context context = ZMQ.context(1);


       Socket clients = context.socket(ZMQ.ROUTER);
       clients.bind ("tcp://*:5559");


       Socket workers = context.socket(ZMQ.DEALER);
       workers.bind ("inproc://workers");


       for(int thread_nbr = 0; thread_nbr < 5; thread_nbr++) {
           Thread worker = new Worker (context,thread_nbr);
           worker.start();
       }
       //  Connect work threads to client threads via a queue
       ZMQ.proxy (clients, workers, null);


       //  We never get here but clean up anyhow
       clients.close();
       workers.close();
       context.term();
   }

  • 服务端启动一组worker线程,每个worker创建一个REP套接字,并处理收到的请求。worker线程就像是一个单线程的服务,唯一的区别是使用了inproc而非tcp协议,以及绑定-连接的方向调换了。
  • 服务端创建ROUTER套接字用以和client通信,因此提供了一个TCP协议的外部接口。
  • 服务端创建DEALER套接字用以和worker通信,使用了内部接口(inproc)。
  • 服务端启动了QUEUE内部装置,连接两个端点上的套接字。QUEUE装置会将收到的请求分发给连接上的worker,并将应答路由给请求方。

eclipse下maven项目工程下载地址,可直接运行:点击打开链接

三、Zeromq高级

1.请求-应答信封原理

ZMQ消息通讯中信封机制是其中的核心,我们在使用req-rep时只是进行一次请求发送,然后进行一次请求处理,但是实际req端在进行请求发送时的数据格式是这样的:


可见,虽然我们只是发送了一帧的数据,实际上ZMQ发送了两帧的数据,第一帧是空数据,代表分隔符,第二帧才是我们要发送的数据,同时在rep端也是接收的两帧数据,只不过ZMQ自动解信封,只获取数据帧,返回到我们的程序,然后,通用也是发送两个数据帧:


在实际程序中我们也只是发送了数据帧而已,空帧是ZMQ自动加上的,req端接收后自动解信封,获取数据帧。

我们知道Router和Dealer是Req-Rep的高级模式,使得可以实现多对多的通讯模式,其中Router和Dealer的信封机制是含有三个帧,我们以多对多通讯模式为例:

zeromq简介及各个通讯模式实例详解(附java实现)_第5张图片

其中router接收来自req的请求,其具体帧如下:

zeromq简介及各个通讯模式实例详解(附java实现)_第6张图片

第一帧是地址,即标定请求的来源,这个地址并不是req发送来的,而是router自己添加的,另外这个地址可以再req端进行设置request.setIdentity(identity.getBytes());如果req端没有进行地址的设定,那么router端将自动进行设置uuid编码,代表其地址;第二帧同样是空分割符号,第三帧才是数据帧。

router接下来将这三帧数据继续写入到Dealer,dealer同样接收的也是三帧数据,

然后dealer将数据发送到rep,

rep端自动解析出数据帧,并进行反馈,其中地址就是保留的req地址,

反馈后,dealer端收到三帧:

zeromq简介及各个通讯模式实例详解(附java实现)_第7张图片

其中第一帧也是地址,第二帧空白分隔符,第三帧是数据帧,

接下来dealer继续将三帧写入Router,

router然后在发送到req,

整个过程结束。

总结来说,req-rep每一次的请求应答都是只接收或者发送一帧数据,实际ZMQ是包装了两帧数据,而router-dealer每次的交互是三帧数据,第一帧是地址,第二帧是空白符号,第三帧是数据帧,其中地址帧可以在请求端设置,也可以不设置,但是第二帧的空白符号不能省略,这里dealer可以理解成req端,roouter可以理解成req端。

2.负载均衡

负载均衡就是对多请求的一个缓存问题,中间层就是负载均衡的代理,一端是请求,另一端是对请求的相应。其中请求响应端主动去中间层获取请求,然后处理并相应。ZMQ的负载均衡模式如下:

zeromq简介及各个通讯模式实例详解(附java实现)_第8张图片

这里由两个Router来作为中间层,具体的数据流程如下:

(1)中间层启动,Worker连接Backend,向其发送Request请求(ready),这个时候中间层就能够知道哪一个worker现在是空闲的,将其保存起来(放到worker队列),可以处理请求

worker的执行流程就是send(发送ready)--->recv(获取请求),

(2)Client端向Fronted发送请求,中间层将请求缓存到一个任务队列

(3)中间层从任务队里里面取出一个任务,将其发送给worker队列中的一个worker,并将其从woker队列中移除

(4)worker处理完以后,发送执行结果,也就是send,中间层收到woker的数据 之后,将其发送给相应的client,然后在讲这个worker放到worker队列中,表示当前这个worker可用。

eclipse下maven项目工程负载均衡实例下载地址,可直接运行:点击打开链接




你可能感兴趣的:(zeromq,jeromq,消息队列,网络通信)