作为职场新人,之前在看公司相关项目代码的时候,发现公司的在用的一个框架代码中用到了很多之前没有接触过得框架,所以打算专门开设几个系列来重点学习总结一下这些框架的相关知识。本系列我们主要介绍的是高速并发消息通信框架——ZeroMQ
官方文档:
ZeroMQzeromq.org这里推荐一个非常好的中文博客(主要是官方文档的翻译版本,同时也加入了译者的一些实践与思考,推荐感兴趣的小伙伴阅读)
chapter1.md · 乌合之众/ZeroMQ-Guide-Zh - 码云 Gitee.comgitee.com一、ZeroMQ简介
ZMQ看起来像是一个嵌入式网络连接库,但实际上是一个并发框架。框架提供的套接字可以满足在多种协议之间传输原子信息,如线程间、进程间、TCP、广播等。可以使用ZMQ构建多对多的连接方式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的高速使得它能胜任分布式应用。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。ZMQ有着多语言支持,并能在几乎所有的操作系统上运行。
二、请求应答模式
在学习任何编程语言的时候,我们都会从最简单的helloWorld程序开始,拿在ZMQ中也不例外,在本例中我们需要启动一个server和一个client,client端负责向server端发送hello字符串,server端回复world字符串,整体结构如下:
使用REQ-REP套接字发送和接受消息是需要遵循一定规律的,client需要依次调用zmq_send()和zmq_recv(),如果打破了这个秩序(比如连续发送两次)将会报错返回-1;同样的在server端也是需要依次调用zmq_recv()和zmq_send()。
具体的,server端的C++代码如下:
//
// Hello World server in C++
// Binds REP socket to tcp://*:5555
// Expects "Hello" from client, replies with "World"
//
#include <zmq.hpp>
#include <string>
#include <iostream>
#ifndef _WIN32
#include <unistd.h>
#else
#include <windows.h>
#define sleep(n) Sleep(n)
#endif
int main () {
// Prepare our context and socket
zmq::context_t context (1);
zmq::socket_t socket (context, ZMQ_REP);
socket.bind ("tcp://*:5555");
while (true) {
zmq::message_t request;
// Wait for next request from client
socket.recv (&request);
std::cout << "Received Hello" << std::endl;
// Do some 'work'
sleep(1);
// Send reply back to client
zmq::message_t reply (5);
memcpy (reply.data (), "World", 5);
socket.send (reply);
}
return 0;
}
client端的C++代码如下:
//
// Hello World client in C++
// Connects REQ socket to tcp://localhost:5555
// Sends "Hello" to server, expects "World" back
//
#include <zmq.hpp>
#include <string>
#include <iostream>
int main ()
{
// Prepare our context and socket
zmq::context_t context (1);
zmq::socket_t socket (context, ZMQ_REQ);
std::cout << "Connecting to hello world server…" << std::endl;
socket.connect ("tcp://localhost:5555");
// Do 10 requests, waiting each time for a response
for (int request_nbr = 0; request_nbr != 10; request_nbr++) {
zmq::message_t request (5);
memcpy (request.data (), "Hello", 5);
std::cout << "Sending Hello " << request_nbr << "…" << std::endl;
socket.send (request);
// Get the reply.
zmq::message_t reply;
socket.recv (&reply);
std::cout << "Received World " << request_nbr << std::endl;
}
return 0;
}
上述代码中涉及到的一些zmq_context_t、amz_socket_t等类型都会在后续介绍到
三、获取版本号
在使用ZMQ的过程中可能会遇到一些bug,这些bug可能会在后续的版本中修复,所以有必要指导如何获取当前使用ZMQ的版本信息,具体参考如下代码
int main ()
{
s_version ();
return EXIT_SUCCESS;
}
四、发布-订阅模式
在上述的请求-应答模式中我们构建了一对一的通信方式,下面会介绍一对多的通信方式,具体的例子是一个温度分发系统,server端会测量温度、湿度等环境信息(随机生成数字),并同步给其他所有client,具体系统架构如下所示:
server端代码如下:
//
// Created by 梁潇 on 2019-07-30.
//
#include "WeatherUpdateServer.h"
#include <zmq.hpp>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#if (defined (WIN32))
#include <zhelpers.hpp>
#endif
#define within(num) (int) ((float) num * random () / (RAND_MAX + 1.0))
int main () {
// Prepare our context and publisher
zmq::context_t context (1);
zmq::socket_t publisher (context, ZMQ_PUB);
publisher.bind("tcp://*:5556");
publisher.bind("ipc://weather.ipc"); // Not usable on Windows.
// Initialize random number generator
srandom ((unsigned) time (NULL));
while (1) {
int zipcode, temperature, relhumidity;
// Get values that will fool the boss
zipcode = within (100000);
temperature = within (215) - 80;
relhumidity = within (50) + 10;
// Send message to all subscribers
zmq::message_t message(20);
snprintf ((char *) message.data(), 20 ,
"%05d %d %d", zipcode, temperature, relhumidity);
publisher.send(message);
}
return 0;
}
client端代码如下:
//
// Created by 梁潇 on 2019-07-30.
//
#include <zmq.hpp>
#include <iostream>
#include <sstream>
int main(int argc,char* argv[]){
zmq::context_t context(1);
std::cout << "Collecting updates from weather server…n" << std::endl;
zmq::socket_t subscriber(context,ZMQ_SUB);
subscriber.connect("tcp://localhost:5556");
const char *filter = (argc > 1)? argv [1]: "10001 ";
subscriber.setsockopt(ZMQ_SUBSCRIBE,filter,strlen(filter));
int update_nbr;
long total_temp = 0;
for(update_nbr=0;update_nbr<100;update_nbr++){
zmq::message_t update;
int zipcode, temperature, relhumidity;
subscriber.recv(&update);
std::istringstream iss(static_cast<char*>(update.data()));
iss >> zipcode >> temperature >> relhumidity ;
total_temp += temperature;
}
std::cout << "Average temperature for zipcode '"<< filter
<<"' was "<<(int) (total_temp / update_nbr) <<"F"
<< std::endl;
return 0;
}
在使用订阅socket的时候,必须利用setsockopt()设置订阅,否则将会收不到信息,当client更新一项通知的时候,所有订阅了的client都会收到。订阅信息可以是任何字符串,可以设置多次。只要消息满足其中一条订阅信息,SUB套接字就会收到。订阅者可以选择不接收某类消息,也是通过zmq_setsockopt()方法实现的。PUB-SUB套接字组合是异步的。客户端在一个循环体中使用zmq_recv()接收消息,如果向SUB套接字发送消息则会报错;类似地,服务端可以不断地使用zmq_send()发送消息,但不能在PUB套接字上使用zmq_recv()。关于PUB-SUB套接字,还有一点需要注意:你无法得知SUB是何时开始接收消息的。就算你先打开了SUB套接字,后打开PUB发送消息,这时SUB还是会丢失一些消息的,因为建立连接是需要一些时间的(三次握手),这个建立连接消耗的时间很少,但并不是零。后续会给出正确的同步方式。
关于发布-订阅模式的几点说明:
- 订阅者可以连接多个发布者,轮流接收消息;
- 如果发布者没有订阅者与之相连,那它发送的消息将直接被丢弃;
- 如果你使用TCP协议,那当订阅者处理速度过慢时,消息会在发布者处堆积。以后我们会讨论如何使用阈值(HWM)来保护发布者。
- 在目前版本的ZMQ中,消息的过滤是在订阅者处进行的。也就是说,发布者会向订阅者发送所有的消息,订阅者会将未订阅的消息丢弃。
五、分布式处理
下面一个示例程序中,我们将使用ZMQ进行超级计算,也就是并行处理模型:
- 任务分发器会生成大量可以并行计算的任务;
- 有一组worker会处理这些任务;
- 结果收集器会在末端接收所有worker的处理结果,进行汇总。
现实中,worker可能散落在不同的计算机中,利用GPU(图像处理单元)进行复杂计算。下面是任务分发器的代码,它会生成100个任务,任务内容是让收到的worker延迟若干毫秒。整体系统架构如下图所示
具体的,ventilator的C++代码如下
#include <zmq.hpp>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#define within(num) (int) ((float) num * random () / (RAND_MAX + 1.0))
int main (int argc, char *argv[])
{
zmq::context_t context (1);
// Socket to send messages on
zmq::socket_t sender(context, ZMQ_PUSH);
sender.bind("tcp://*:5557");
std::cout << "Press Enter when the workers are ready: " << std::endl;
getchar ();
std::cout << "Sending tasks to workers…n" << std::endl;
// The first message is "0" and signals start of batch
zmq::socket_t sink(context, ZMQ_PUSH);
sink.connect("tcp://localhost:5558");
zmq::message_t message(2);
memcpy(message.data(), "0", 1);
sink.send(message);
// Initialize random number generator
srandom ((unsigned) time (NULL));
// 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 = within (100) + 1;
total_msec += workload;
message.rebuild(10);
memset(message.data(), '0', 10);
sprintf ((char *) message.data(), "%d", workload);
sender.send(message);
}
std::cout << "Total expected cost: " << total_msec << " msec" << std::endl;
sleep (1); // Give 0MQ time to deliver
return 0;
}
worker的C++代码如下
//
// Task worker in C++
// 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
//
// Olivier Chamoux <olivier.chamoux@fr.thalesgroup.com>
//
#include "zhelpers.hpp"
#include <string>
int main (int argc, char *argv[])
{
zmq::context_t context(1);
// Socket to receive messages on
zmq::socket_t receiver(context, ZMQ_PULL);
receiver.connect("tcp://localhost:5557");
// Socket to send messages to
zmq::socket_t sender(context, ZMQ_PUSH);
sender.connect("tcp://localhost:5558");
// Process tasks forever
while (1) {
zmq::message_t message;
int workload; // Workload in msecs
receiver.recv(&message);
std::string smessage(static_cast<char*>(message.data()), message.size());
std::istringstream iss(smessage);
iss >> workload;
// Do the work
s_sleep(workload);
// Send results to sink
message.rebuild();
sender.send(message);
// Simple progress indicator for the viewer
std::cout << "." << std::flush;
}
return 0;
}
sink的C++代码如下
//
// Task sink in C++
// Binds PULL socket to tcp://localhost:5558
// Collects results from workers via that socket
//
// Olivier Chamoux <olivier.chamoux@fr.thalesgroup.com>
//
#include <zmq.hpp>
#include <time.h>
#include <sys/time.h>
#include <iostream>
int main (int argc, char *argv[])
{
// Prepare our context and socket
zmq::context_t context(1);
zmq::socket_t receiver(context,ZMQ_PULL);
receiver.bind("tcp://*:5558");
// Wait for start of batch
zmq::message_t message;
receiver.recv(&message);
// Start our clock now
struct timeval tstart;
gettimeofday (&tstart, NULL);
// Process 100 confirmations
int task_nbr;
int total_msec = 0; // Total calculated cost in msecs
for (task_nbr = 0; task_nbr < 100; task_nbr++) {
receiver.recv(&message);
if ((task_nbr / 10) * 10 == task_nbr)
std::cout << ":" << std::flush;
else
std::cout << "." << std::flush;
}
// Calculate and report duration of batch
struct timeval tend, tdiff;
gettimeofday (&tend, NULL);
if (tend.tv_usec < tstart.tv_usec) {
tdiff.tv_sec = tend.tv_sec - tstart.tv_sec - 1;
tdiff.tv_usec = 1000000 + tend.tv_usec - tstart.tv_usec;
}
else {
tdiff.tv_sec = tend.tv_sec - tstart.tv_sec;
tdiff.tv_usec = tend.tv_usec - tstart.tv_usec;
}
total_msec = tdiff.tv_sec * 1000 + tdiff.tv_usec / 1000;
std::cout << "nTotal elapsed time: " << total_msec << " msecn" << std::endl;
return 0;
}
上述代码中涉及到的一些细节问题如下:
1、worker上游和ventilator相连,下游和sink相连,这就意味着你可以随意的增加workder,只要正确配置相应的配置就可以动态的增加,如果worker是邦定至端点的,那么每当增加新的worker的时候,ventilator和sink都需要更改相应的配置才能使得能与其正常通信。因为ventilator和sink是整个网络结构中比较稳定的部分,不会经常的发生改变,所以这两部分是需要邦定至端点的,而worker通常在网络中是动态变化的,所以是连接至端点
2、需要进行一些同步的工作,等到所有worker启动之后再进行任务的分发,因为worker连接至端点的操作是比较耗时的,一旦一个worker成功连接至上游的ventilator,那么一瞬间所有的任务都会分配给当前的这个worker,所以需要同步的机制
3、ventilator使用PUSH套接字向向workers分配任务,这里面涉及到负载均衡的相关内容,后续会提到
4、sink的PULL套接字会均衡的从各个workers收集信息,这里面涉及到公平队列的机制,具体公平队列机制如下
欢迎对技术感兴趣的小伙伴们向专栏投稿:
独立团丶zhuanlan.zhihu.com欢迎感兴趣的小伙伴关注公众号:独立团丶