程序跟人一样需要射交,不好意思是社交。那么程序也需要互通,我们有UDP,TCP,HTTP,IPX,WebSocket和其他相关协议来连接应用程序。
但是啊如此底层的协议要实现是比较难的。我们需要一个高度抽象,可扩展,以及容易使用的东西,它就是ZeroMQ,它给予了我们高级级别可用性以及低级别的速度。
消息队列,技术又称FIFO(First In First Out)队列是一个基础简单的数据结构。有各种不同的队列实现,例如优先队列,双端队列。它们具有不同特性,大部分的目的是加入到一个队列中,然后再需要的时候取走。
想象一下,当我们在内存中存放着队列(苍老师无码全集),如果断电或者硬件故障了,那么整个队列也就丢失了,那么另外一个要获取该队列的程序就取不到任何消息。(可惜了)
然而使用消息队列可以保证消息发送到目的地,无论发生了什么。消息队列允许在松耦合的组建中异步通讯,而且提供了强力的一致性。当在资源不足的情况下,可以防止你立即处理数据,你可以将它们存入消息队列服务器,那么你可以在后续对其进行接受消息。(可以一个片子一个片子下载了)
消息队列还在大规模分布式系统中扮演中重要角色,允许你进行异步通讯。我们来了解一下同步和异步机制。
在通常的同步系统中,同一时间只处理一个任务(task)。如果一个任务(task)没结束就不会进行下一个任务(task),这是处理事情比较简单的方法。
我们可以通过线程(threads),这样就可以并行执行任务了。
在线程模型中,线程被操作系统通过单处理器或多处理器管理
异步输入输出(AIO, Asynchronous Input/Output)允许程序在处理IO请求时进行做其他事情。AIO广泛用于实时应用,通过使用AIO,我们可以在单个线程交替执行多个任务。
传统的编程方式是执行一个进行然后等待结束,这种方法的穷点是在执行过程中容易阻塞。然后,AIO就不同了,在AIO,一个任务不依赖进程仍可以继续。我们将在后续讨论使用ZeroMQ实现AIO。
你也许想知道为什么使用消息队列而不是使用进程通过使用单线程队列,或者多线程队列。让我们考虑一个场景,你有个web应用程序类似Google Images,当用户输入URLs提交表单,你的应用程序会从该URLs获取所有的图片。然而:
当前场景下,你需要将URLs放入消息队列,然后进行处理。那么你就需要个消息队列系统。
社区是这样定义ZeroMQ的,"sockets on steroids"(打了激素的套接字)。官方是这样定义的:ZeroMQ是一个消息库,用来帮助开发者构建分布式以及并行应用程序。
我们首先要知道的是ZeroMQ并非传统类型的消息队列系统,例如ActiveMQ,WebSphereMQ, 或RabbitMQ。ZeroMQ是不同,它提供了我们自己构建消息队列系统的工具,所以它是library。
它可以运行在不同的架构中,从ARM到Itanium,且已支持超过20门语言。
ZeroMQ简单。我们可以做异步I/O操作,ZeroMQ可以将消息放入I/O线程。ZeroMQ I/O线程处理网络时是异步的,所以它可以做其余的这些工作。如果你用过socket,你应该知道做这事情很头疼。然后,ZeroMQ让使用sockets变得so easy。
ZeroMQ非常快。Second Life网站达到13.4微秒传输,每秒达到4,100,000条消息。ZeroMQ可以使用组播,允许高效传输到多个目的。
不像传统消息队列系统,ZeroMQ是brokerless.传统消息系统中,在网络中存有一个中央消息服务器(broker),所有的node都连接到这个中央node,每个node与其他node通讯都需要通过该中央broker。它们之间不直接进行通讯。
然而,ZeroMQ是brokerless,在brokerless设计中,应用可以直接进行相互通许,不需要通过中间再有个broker。我们后面深入讨论。
注意:ZeroMQ并不把消息存放硬盘,但是可以通过设置zmq.SWAP使用本地swap文件来保存消息
我们使用hello world来开始我们的ZeroMQ
我们考虑一个场景:我们具有一个服务端和客户端,服务端收到hello就返回一个world给客户端。服务运行在4040端口,客户端将消息发往4040端口
注意:请自行安装zmq库
以下是服务端代码,收到消息后返回world
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "zmq.h"
int main(int argc, char* argv[])
{
void* context = zmq_ctx_new();
void* respond = zmq_socket(context, ZMQ_REP);
zmq_bind(respond, "tcp://*:4040");
printf("Starting...\n");
for (;;) {
zmq_msg_t request;
zmq_msg_init(&request);
zmq_msg_recv(&request, respond, 0);
printf("Received: hello\n");
zmq_msg_close(&request);
sleep(1); // sleep on second
zmq_msg_t reply;
zmq_msg_init_size(&reply, strlen("world"));
memcpy(zmq_msg_data(&reply), "world", 5);
zmq_msg_send(&reply, respond, 0);
zmq_msg_close(&reply);
}
zmq_close(respond);
zmq_ctx_destroy(context);
return 0;
}
以下是客户端代码,发送hello消息给服务端
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "zmq.h"
int main(int argc, char *argv[])
{
void* context = zmq_ctx_new();
printf("Client Starting...\n");
void* request = zmq_socket(context, ZMQ_REQ);
zmq_connect(request, "tcp://localhost:4040");
int count = 0;
for (;;) {
zmq_msg_t req;
zmq_msg_init_size(&req, strlen("hello"));
memcpy(zmq_msg_data(&req), "hello", 5);
printf("Sending: hello - %d\n", count);
zmq_msg_send(&req, request, 0);
zmq_msg_close(&req);
zmq_msg_t reply;
zmq_msg_init(&reply);
zmq_msg_recv(&reply, request, 0);
printf("Received: hello - %d\n", count);
zmq_msg_close(&reply);
count++;
}
// We nerver get here though
zmq_close(request);
zmq_ctx_destroy(context);
return 0;
}
我们看看执行结果
$ gcc -o server server.c -lzmq
$ gcc -o client client.c -lzmq
$ ./server
Starting...
Received: hello
Received: hello
Received: hello
Received: hello
Received: hello
Received: hello
Received: hello
Received: hello
Received: hello
$ ./client
Client Starting...
Sending: hello - 0
Received: hello - 0
Sending: hello - 1
Received: hello - 1
Sending: hello - 2
Received: hello - 2
Sending: hello - 3
Received: hello - 3
Sending: hello - 4
Received: hello - 4
Sending: hello - 5
Received: hello - 5
我们实现了一个基本request-reply的架构。
首先我们创建了一个context和socket。zmq_ctx_new()方法创建一个新的context.它是线程安全,使用一个context可以在多线程中使用。
zmq_socket()在定义的context中创建一个socket。ZeroMQ的socket为非线程安全,所有它应该只被创建它的线程所使用。传统的sockets是同步的,而ZeroMQ的sockets在客户端和服务端都有个队列来管理request-reply模式的异步操作。ZeroMQ自动协商连接
,重连,断开连接,内容传输。我们后面讨论ZeroMQ和传统sockets有什么具体不同。
服务端绑定了ZMQ_REP在4040端口,等待请求,在收到任何消息后都将返回。
"hello world"例子是我们第一个模式,那就是request-reply模式。
我们使用了request-reply模式从客户端发送一个或多个消息,并代码每一次的回应。在ZeroMQ好像是最简单的使用方式了。请求和响应是严格序列的。
下面是request_reply的reply部分
void* context = zmq_ctx_new();
void* respond = zmq_socket(context, ZMQ_REP);
zmq_bind(respond, "tcp://*:4040");
服务端使用ZMQ_REP socket来接受消息,并且回复给客户端。如果客户端和服务区连接中断,那么回复的消息将被丢弃,没有任何提醒。进来的路由策略是fair-queue,出去的路由策略是last-peer
我们大部分都在讨论queue。你可能想知道什么是fair-queue策略。它是一个调度算法,通过定义来合理分配资源。
要理解如何工作,我假设输入每秒有16,2,6和8个packets,但是输出只能处理12packets每秒。在有4个flow情况下我们可以每秒发送4个packets,但是flow2只需要2个packets每秒。fair-queue的规则就是不能有任何空闲输出,除非所有输入为空闲。所以,我们可以让flow传输2个packets每秒,将多余的10个packets分享给其余的flow。
这就是ZMQ_REP的进入路由策略。round-robin调度是最简单实现fair-queue策略的方法,ZeroMQ也就是这么干的。
以下是request-reply模式的request部分
void* context = zmq_ctx_new();
printf("Client Starting...\n");
void* request = zmq_socket(context, ZMQ_REQ);
zmq_context(request, "tcp://localhost:4040");
客户端使用ZMQ_REQ都发送消息,并且从服务端取得小弟,所有的消息都使用round-robin路由策略。进入路由策略是last-peer.
ZMQ_REQ不会丢掉任何消息。如果没有任何可用的服务来发送消息,或者所有的服务处于忙碌状态,所有的发送zmq-send()操作都会阻塞,直到有可用的服务发送消息为止。ZMQ_REQ兼容ZMQ_REP和ZMQ_ROUTE类型,我们后面讨论ZMQ_ROUTER.
我们来看看如何请求消息以及发送消息
printf("Sending: hello - %d\n", count);
zmq_msg_send(&req, request, 0);
zmq_msg_close(&req);
客户端通过zmq_msg_send()发送消息给服务端,它将消息放入队列,然后发送给socket。
int zmq_send_msg(zmq_msg_t *msg, void *socket, int flags)
zmq_msg_send接收三个参数,message, socket, flags,消息发往网络,并不会有成功标识。flags可选值为ZMQ_DONTWAIT,ZMQ_SNDMORE.ZMQ_DONTWAIT意为异步消息,ZMQ_SNDMORE意为该消息为多个消息的一部分
在发送了消息后,客户端等待响应,通过使用zmq_msg_recv()实现
zmq_msg_recv(&reply, request, 0);
printf("Received: hello - %d\n", count);
zmq_msg_close(&reply);
zmq_msg_recv()从socket接受小哦戏,保存在zmq_msg_t中
int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags)
flags也可以设为ZMQ_DONTWAIT
不同的编程语言有不同的操作字符串方式。Erlang甚至都没有字符串。在C语言中,字符串为null-terminated的,String仅仅是字符数组,通过使用\0标识字符串终止。用C操作字符串容易出错,这是众所周知的事情,所以我们在C中使用字符串时要非常小心。
当使用ZeroMQ发送消息时,你自己有责任来保证它的格式安全,以便让其他程序可以读取。ZeroMQ仅仅知道消息大小。
在应用中我们有可能会使用多个不同的编程语言,一个语言可能并不会添加null-byte到字符串末尾,那么当于C语言通讯可能会得到奇怪的结果
你可以使用如下发送null byte的消息
zmq_msg_init_data_(&request, "world", 6, NULL, NULL);
你也可能通过使用Erlang如下发送消息:
erlzmq:send(Request, <<"world">>)
也就是C客户端连接使用Erlang写的服务端,我们发送world到服务端,在该情况下Erlang将收到world,如果我们发送消息包含有null byte,Erlang就会收到[119,111,114,108,100,0],而不是字符串,而是一序列数组。这些这些数字都是ASCII编码字符,并没有解释为字符串。
C语言中你不能依赖ZeroMQ传输的消息字符串是否是安全终止的。
ZeroMQ通过使用长度并且不带null byte的字符串来解决。所以,ZeroMQ传递bytes数据还有他的长度。
我们来看看我们的ZeroMQ版本,因为ZeroMQ2.x和ZeroMQ3.x以上版本比较大的变化,废弃了一些方法。
#include <stdio.h>
#include "zmq.h"
int main(int argc, char const *argv[])
{
int major, minor, patch;
zmq_version(&major, &minor, &patch);
printf("Installed ZeroMQ version: %d.%d.%d\n", major, minor, patch);
return 0;
}
./version
Installed ZeroMQ version: 4.0.5
操,我竟然装了个4.x版本
client.py
import zmq
import time
context = zmq.Context()
print("Client Starting...");
request = context.socket(zmq.REQ)
request.connect("tcp://localhost:4040")
count = 0
while True:
print("Sending: hello - %d" % count)
request.send_string('hello')
recv_str = request.recv_string()
print("Received: hello - %d" % count)
count += 1
request.close()
context.destroy()
server.py
import zmq
import time
context = zmq.Context()
respond = context.socket(zmq.REP)
respond.bind("tcp://*:4040")
print("Starting...")
while True:
recv_str = respond.recv_string()
print("Received: %s" % recv_str)
time.sleep(1)
respond.send_string('world')
respond.close()
context.destroy()