我们为什么须要ZMQ
眼下的应用程序非常多都会包括跨网络的组件,不管是局域网还是因特网。这些程序的开发人员都会用到某种消息通信机制。有些人会使用某种消息队列产品,而大多数人则会自己手工来做这些事,使用TCP或UDP协议。
这些协议使用起来并不困难。可是,简单地将消息从A发给B,和在不论什么情况下都能进行可靠的消息传输,这两种情况显然是不同的。
让我们看看在使用纯TCP协议进行消息传输时会遇到的一些典型问题。不论什么可复用的消息传输层肯定或多或少地会要解决下面问题:
怎样处理I/O?是让程序堵塞等待响应,还是在后台处理这些事?这是软件设计的关键因素。堵塞式的I/O操作会让程序架构难以扩展,而后台处理I/O也是比較困难的。
怎样处理那些暂时的、来去自由的组件?我们是否要将组件分为client和服务端两种。并要求服务端永不消失?那假设我们想要将服务端相连怎么办?我们要每隔几秒就进行重连吗?
我们如何表示一条消息?我们如何通过拆分消息,让其变得易读易写,不用操心缓存溢出,既能高效地传输小消息,又能胜任视频等大型文件的传输?
怎样处理那些不能立马发送出去的消息?比方我们须要等待一个网络组件又一次连接的时候?我们是直接丢弃该条消息,还是将它存入数据库。或是内存中的一个队列?
要在哪里保存消息队列?假设某个组件读取消息队列的速度非常慢,造成消息的堆积怎么办?我们要採取什么样的策略?
怎样处理丢失的消息?我们是等待新的数据。请求重发,还是须要建立一套新的可靠性机制以保证消息不会丢失?假设这个机制自身崩溃了呢?
假设我们想换一种网络连接协议,如用广播取代TCP单播?或者改用IPv6?我们是否须要重写全部的应用程序,或者将这样的协议抽象到一个单独的层中?
我们怎样对消息进行路由?我们能够将消息同一时候发送给多个节点吗?能否将应答消息返回给请求的发送方?
我们怎样为还有一种语言写一个API?我们是否须要全然重写某项协议,还是又一次打包一个类库?
如何才干做到在不同的架构之间传送消息?是否须要为消息规定一种编码?
我们怎样处理网络通信错误?等待并重试。还是直接忽略或取消?
ZMQ就是这样一种软件:它高效,提供了嵌入式的类库。使应用程序可以非常好地在网络中扩展,成本低廉。
ZMQ的主要特点有:
ZMQ会在后台线程异步地处理I/O操作。它使用一种不会死锁的数据结构来存储消息。
网络组件能够来去自如,ZMQ会负责自己主动重连,这就意味着你能够以不论什么顺序启动组件;用它创建的面向服务架构(SOA)中,服务端能够任意地增加或退出网络。
ZMQ会在有必要的情况下自己主动将消息放入队列中保存,一旦建立了连接就開始发送。
ZMQ有阈值(HWM)的机制,能够避免消息溢出。
当队列已满,ZMQ会自己主动堵塞发送者,或丢弃部分消息。这些行为取决于你所使用的消息模式。
ZMQ能够让你用不同的通信协议进行连接,如TCP、广播、进程内、进程间。改变通信协议时你不须要去改动代码。
ZMQ会恰当地处理速度较慢的节点,会依据消息模式使用不同的策略。
ZMQ提供了多种模式进行消息路由,如请求-应答模式、公布-订阅模式等。
这些模式能够用来搭建网络拓扑结构。
ZMQ中能够依据消息模式建立起一些中间装置(非常小巧)。能够用来减少网络的复杂程度。
ZMQ会发送整个消息,使用消息帧的机制来传递。假设你发送了10KB大小的消息,你就会收到10KB大小的消息。
ZMQ不强制使用某种消息格式,消息能够是0字节的,或是大到GB级的数据。
当你表示这些消息时,能够选用诸如谷歌的protocol buffers,XDR等序列化产品。
ZMQ可以智能地处理网络错误,有时它会进行重试,有时会告知你某项操作发生了错误。
ZMQ甚至能够减少对环境的污染,由于节省了CPU时间意味着节省了电能。
事实上ZMQ可以做的还不止这些,它会颠覆人们编写网络应用程序的模式。尽管从表面上看。它只是是提供了一套处理套接字的API。可以用zmq_recv()和zmq_send()进行消息的收发,可是,消息处理将成为应用程序的核心部分,非常快你的程序就会变成一个个消息处理模块。这既美观又自然。它的扩展性还非常强。每项任务由一个节点(节点是一个线程)、同一台机器上的两个节点(节点是一个进程)、同一网络上的两台机器(节点是一台机器)来处理,而不须要修改应用程序。
一、ZeroMQ的背景介绍
官方:“ZMQ(下面ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。
ZMQ的明白目标是“成为标准网络协议栈的一部分,之后进入Linux内核”。
如今还未看到它们的成功。可是,它无疑是极具前景的、而且是人们更加须要的“传统”BSD套接字之上的一层封装。
ZMQ让编写高性能网络应用程序极为简单和有趣。”
与其它消息中间件相比。ZMQ并不像是一个传统意义上的消息队列server。其实,它也根本不是一个server,它更像是一个底层的网络通讯库。在Socket API之上做了一层封装,将网络通讯、进程通讯和线程通讯抽象为统一的API接口。
二、ZMQ是什么
阅读了ZMQ的Guide文档后。我的理解是。这是个类似于Socket的一系列接口,他跟Socket的差别是:普通的socket是端到端的(1:1的关系),而ZMQ却是能够N:M 的关系,人们对BSD套接字的了解较多的是点对点的连接,点对点连接须要显式地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而ZMQ屏蔽了这些细节。让你的网络编程更为简单。ZMQ用于node与node间的通信,node能够是主机或者是进程。
三、三种模型
參考网址:http://blog.csdn.net/whuqin/article/details/8442919/
a) 应答模式:
使用REQ-REP套接字发送和接受消息是须要遵循一定规律的。client首先使用zmq_send()发送消息,再用zmq_recv()接收。如此循环。
假设打乱了这个顺序(如连续发送两次)则会报错。类似地,服务端必须先进行接收,后进行发送。
b) 订阅公布模式
PUB-SUB套接字组合是异步的。client在一个循环体中使用recv ()接收消息。假设向SUB套接字发送消息则会报错;类似地,服务端能够不断地使用send ()发送消息,但不能再PUB套接字上使用recv ()。
关于PUB-SUB套接字,另一点须要注意:你无法得知SUB是何时開始接收消息的。就算你先打开了SUB套接字。后打开PUB发送消息。这时SUB还是会丢失一些消息的。由于建立连接是须要一些时间的。非常少。但并非零。解决此问题须要在PUB端增加sleep。
c) 基于分布式处理(管道模式)
这篇博客对ZMQ有一个初步的介绍。下篇博客介绍怎样通过JAVA来调用ZMQ实现消息处理。
以下贴出PUB_SUB(应答模式)模式下的代码:
公布端:
package cn.edu.ujn.pub_sub;
import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Context;
import org.zeromq.ZMQ.Socket;
/**
* Pubsub envelope publisher
*/
public class psenvpub {
public static void main (String[] args) throws Exception {
// Prepare our context and publisher
Context context = ZMQ.context(1);
Socket publisher = context.socket(ZMQ.PUB);
publisher.bind("tcp://*:5563");
while (!Thread.currentThread ().isInterrupted ()) {
// Write two messages, each with an envelope and content
publisher.sendMore ("A");
publisher.send ("We don't want to see this");
publisher.sendMore ("B");
publisher.send("We would like to see this");
}
publisher.close ();
context.term ();
}
}
公布端须要通过context.socket(ZMQ.PUB)表示为公布端。通过bind方法来创建公布端连接,等待订阅者连接。
之后通过send方法将数据发送到出去。
之后来写订阅端代码
package cn.edu.ujn.pub_sub;
import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Context;
import org.zeromq.ZMQ.Socket;
/**
* Pubsub envelope subscriber
*/
public class psenvsub {
public static void main (String[] args) {
// Prepare our context and subscriber
Context context = ZMQ.context(1);
Socket subscriber = context.socket(ZMQ.SUB);
subscriber.connect("tcp://localhost:5563");
subscriber.subscribe("B".getBytes());
while (!Thread.currentThread ().isInterrupted ()) {
// Read envelope with address
String address = subscriber.recvStr ();
// Read message contents
String contents = subscriber.recvStr ();
System.out.println(address + " : " + contents);
}
subscriber.close ();
context.term ();
}
}
client通过connect进行连接。之后通过recv来进行数据接收。
以下贴出REQ_REP(订阅公布模式)模式下的代码:
发送端:
package cn.edu.ujn.req_rep;
//
// Hello World server in Java
// Binds REP socket to tcp://*:5555
// Expects "Hello" from client, replies with "World"
//
import org.zeromq.ZMQ;
public class hwserver {
private static int i = 0;
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 " + new String(request) + i++);
// Do some 'work'
Thread.sleep(1000);
// Send reply back to client
String reply = "World";
responder.send(reply.getBytes(), 0);
}
responder.close();
context.term();
}
}
接收端:
package cn.edu.ujn.req_rep;
//
// Hello World client in Java
// Connects REQ socket to tcp://localhost:5555
// Sends "Hello" to server, expects "World" back
//
import org.zeromq.ZMQ;
public class hwclient {
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 != 10; 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();
}
}
以下贴出Para_Pipe(基于分布式处理(管道模式))模式下的代码:
发送端:
package cn.edu.ujn.para_pipe;
import java.util.Random;
import org.zeromq.ZMQ;
//
// Task ventilator in Java
// Binds PUSH socket to tcp://localhost:5557
// Sends batch of tasks to workers via that socket
//
public class taskvent {
public static void main (String[] args) throws Exception {
ZMQ.Context context = ZMQ.context(1);
// Socket to send messages on
ZMQ.Socket sender = context.socket(ZMQ.PUSH);
sender.bind("tcp://*:5557");
// Socket to send messages on
ZMQ.Socket sink = context.socket(ZMQ.PUSH);
sink.connect("tcp://localhost:5558");
System.out.println("Press Enter when the workers are ready: ");
System.in.read();
System.out.println("Sending tasks to workers ");
// The first message is "0" and signals start of batch
sink.send("0", 0);
// Initialize random number generator
Random srandom = new Random(System.currentTimeMillis());
// Send 100 tasks
int task_nbr;
int total_msec = 0; // Total expected cost in msecs
for (task_nbr = 0; task_nbr < 100; task_nbr++) {
int workload;
// Random workload from 1 to 100msecs
workload = srandom.nextInt(100) + 1;
total_msec += workload;
System.out.print(workload + ".");
String string = String.format("%d", workload);
sender.send(string, 0);
}
System.out.println("Total expected cost: " + total_msec + " msec");
Thread.sleep(1000); // Give 0MQ time to deliver
sink.close();
sender.close();
context.term();
}
}
中介端:
package cn.edu.ujn.para_pipe;
import org.zeromq.ZMQ;
//
// Task worker in Java
// Connects PULL socket to tcp://localhost:5557
// Collects workloads from ventilator via that socket
// Connects PUSH socket to tcp://localhost:5558
// Sends results to sink via that socket
//
public class taskwork {
public static void main (String[] args) throws Exception {
ZMQ.Context context = ZMQ.context(1);
// Socket to receive messages on
ZMQ.Socket receiver = context.socket(ZMQ.PULL);
receiver.connect("tcp://localhost:5557");
// Socket to send messages to
ZMQ.Socket sender = context.socket(ZMQ.PUSH);
sender.connect("tcp://localhost:5558");
// Process tasks forever
while (!Thread.currentThread ().isInterrupted ()) {
String string = new String(receiver.recv(0)).trim();
long msec = Long.parseLong(string);
// Simple progress indicator for the viewer
System.out.flush();
System.out.print(string + '.');
// Do the work
Thread.sleep(msec);
// Send results to sink
sender.send("".getBytes(), 0);
}
sender.close();
receiver.close();
context.term();
}
}
接收端:
package cn.edu.ujn.para_pipe;
import org.zeromq.ZMQ;
//
// Task sink in Java
// Binds PULL socket to tcp://localhost:5558
// Collects results from workers via that socket
//
public class tasksink {
public static void main (String[] args) throws Exception {
// Prepare our context and socket
ZMQ.Context context = ZMQ.context(1);
ZMQ.Socket receiver = context.socket(ZMQ.PULL);
receiver.bind("tcp://*:5558");
// Wait for start of batch
String string = new String(receiver.recv(0));
// Start our clock now
long tstart = System.currentTimeMillis();
// Process 100 confirmations
int task_nbr;
int total_msec = 0; // Total calculated cost in msecs
for (task_nbr = 0; task_nbr < 100; task_nbr++) {
string = new String(receiver.recv(0)).trim();
if ((task_nbr / 10) * 10 == task_nbr) {
System.out.print(":");
} else {
System.out.print(".");
}
}
// Calculate and report duration of batch
long tend = System.currentTimeMillis();
System.out.println(" Total elapsed time: " + (tend - tstart) + " msec");
receiver.close();
context.term();
}
}
到此为止公布消息的三种模式就写完了,希望通过这篇博客读者可以对ZMQ有初步的认识和简单有用,希望这篇博客对学习zmq的读者有所帮助。
正确地使用上下文
ZMQ应用程序的一開始总是会先创建一个上下文,并用它来创建套接字。
在C语言中,创建上下文的函数是zmq_init()。
一个进程中仅仅应该创建一个上下文。
从技术的角度来说,上下文是一个容器,包括了该进程下全部的套接字。并为inproc协议提供实现,用以快速连接进程内不同的线程。假设一个进程中创建了两个上下文。那就相当于启动了两个ZMQ实例。假设这正是你须要的。那没有问题,但普通情况下:
在一个进程中使用zmq_init()函数创建一个上下文。并在结束时使用zmq_term()函数关闭它。
假设你使用了fork()系统调用。那每一个进程须要自己的上下文对象。假设在调用fork()之前调用了zmq_init()函数。那每一个子进程都会有自己的上下文对象。通常情况下。你会须要在子进程中做些有趣的事,而让父进程来管理它们。
正确地退出和清理
程序猿的一个良好习惯是:总是在结束时进行清理工作。当你使用像Python那样的语言编写ZMQ应用程序时,系统会自己主动帮你完毕清理。
但假设使用的是C语言。那就须要小心地处理了,否则可能发生内存泄露、应用程序不稳定等问题。
内存泄露仅仅是问题之中的一个,事实上ZMQ是非常在意程序的退出方式的。
个中原因比較复杂,但简单的来说。假设仍有套接字处于打开状态。调用zmq_term()时会导致程序挂起;就算关闭了全部的套接字,假设仍有消息处于待发送状态,zmq_term()也会造成程序的等待。仅仅有当套接字的LINGER选项设为0时才干避免。
我们须要关注的ZMQ对象包含:消息、套接字、上下文。
好在内容并不多,至少在一般的应用程序中是这样:
处理完消息后,记得用zmq_msg_close()函数关闭消息。
假设你同一时候打开或关闭了非常多套接字。那可能须要又一次规划一下程序的结构了;
退出程序时。应该先关闭全部的套接字,最后调用zmq_term()函数,销毁上下文对象。
警告:你的想法可能会被颠覆。
传统网络编程的一个规则是套接字仅仅能和一个节点建立连接。
尽管也有广播的协议,但毕竟是第三方的。当我们认定“一个套接字 = 一个连接”的时候。我们会用一些特定的方式来扩展应用程序架构:我们为每一块逻辑创建线程,该线程独立地维护一个套接字。
但在ZMQ的世界里,套接字是智能的、多线程的,可以自己主动地维护一组完整的连接。
你无法看到它们,甚至不能直接操纵这些连接。当你进行消息的收发、轮询等操作时,仅仅能和ZMQ套接字打交道,而不是连接本身。所以说。ZMQ世界里的连接是私有的,不正确外部开放,这也是ZMQ易于扩展的原因之中的一个。
因为你的代码仅仅会和某个套接字进行通信,这样就能够处理随意多个连接,使用随意一种网络协议。而ZMQ的消息模式又能够进行更为便宜和便捷的扩展。
这样一来,传统的思维就无法在ZMQ的世界里应用了。在你阅读演示样例程序代码的时候。或许你脑子里会想方设法地将这些代码和传统的网络编程相关联:当你读到“套接字”的时候,会觉得它就表示与还有一个节点的连接——这样的想法是错误的;当你读到“线程”时,会觉得它是与还有一个节点的连接——这也是错误的。
假设你是第一次阅读本指南,使用ZMQ进行了一两天的开发(或者更长)。可能会认为疑惑,ZMQ怎么会让事情便得如此简单。你再次尝试用以往的思维去理解ZMQ,但又无功而返。最后,你会被ZMQ的理念所折服,拨云见雾,開始享受ZMQ带来的乐趣。
若朋友们有疑问。可留言,以求共进!