简介
ZMQ被称为史上最快的消息队列,它处于会话层之上,应用层之下,使用后台异步线程完成消息的接受和发送,完美的封装了Socket API,大大简化了编程人员的复杂度。
-
ZMQ发送和接受的是具有固定长度的二进制对象,ZMQ的消息包最大254个字节,前6个字节是协议,然后是数据包。
如果超过255个字节(有一个字节表示包属性),则ZMQ会自动分包传输;而对于TCP Socket,是面向字节流的连接。
-
传统的TCP Socket的连接是1对1的,ZMQ的Socket可以很轻松的实现1对N,N对1和N对N的连接模式。
-
ZMQ使用异步后台线程处理接受和发送请求,这意味着发送完消息,不可以立即释放资源,消息什么时候发送用户是无法控制的,同时,ZMQ自动重连,
这意味着用户可以以任意顺序加入到网络中,服务器也可以随时加入或者退出网络。
模式
ZMQ提供了三种基本的通信方式:
-
请求-回复模式(Request-Reply)
-
发布-订阅模式(Publisher-Subscriber)
-
管道模式(Paraller Pipeline)
安装和使用
官网安装地址:https://zeromq.org/download/
测试程序:
server端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <zmq.h>
int main (void)
{
// Socket to talk to clients
void *context = zmq_ctx_new ();
void *responder = zmq_socket (context, ZMQ_REP);
int rc = zmq_bind (responder, "tcp://*:5555");
assert (rc == 0);
while (1) {
char buffer [10];
zmq_recv (responder, buffer, 10, 0);
printf ("Received Hello\n");
sleep (1); // Do some 'work'
zmq_send (responder, "World", 5, 0);
}
return 0;
}
client端:
// Hello World client
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main (void)
{
printf ("Connecting to hello world server…\n");
/*创建一个新的上下文*/
void *context = zmq_ctx_new ();
void *requester = zmq_socket (context, ZMQ_REQ);
/*通过tcp协议,5555端口,连接本机服务端*/
zmq_connect (requester, "tcp://localhost:5555");
int request_nbr;
for (request_nbr = 0; request_nbr != 10; request_nbr++) {
char buffer [10];
printf ("Sending Hello %d…\n", request_nbr);
zmq_send (requester, "Hello", 5, 0);
zmq_recv (requester, buffer, 10, 0);
printf ("Received World %d\n", request_nbr);
}
zmq_close (requester);
zmq_ctx_destroy (context);
return 0;
}
常用API接口
详细介绍看官网ZMQ API reference
zmq_ctx_new
void = zmq_ctx_new();//创建了一个新的zmq context
zmq context 是线程安全的 可以在多个应用线程之间共享,对于调用方而言不需要额外的锁。如果成功,zmq_ctx_new()函数应当返回一个指向新创建的context的不透明handle。
zmq_socket
void *zmq_socket (void *context, int type);//创建ZMQ socket
第一个参数context是上一个创建zmq_ctx_new()的返回值,第二个参数有ZMQ提供了多种类型(客户端-服务端模式、无线电天线模式、发布订阅模式、管道模式、原生模式、请求-回复模式)。
下面只列举出基本的几种类型:
Request-reply pattern:ZMQ_REQ、ZMQ_REP
Publish-subscribe pattern: ZMQ_SUB、ZMQ_PUB
Pipeline pattern:ZMQ_PUSH、ZMQ_PULL
zmq_bind和zmq_connect
int zmq_bind (void *socket, const char *endpoint); int zmq_connect (void *socket, const char *endpoint);
此函数将套接字绑定到本地的终端,终端是一个字符串,格式为transport://addresss
,transport指定底层使用的协议,address指定要绑定的指定传输方式的地址。
返回值:如果成功,zmq_bind()函数返回零。否则,它返回-1并设置errno的值。
PUB和SUB谁bind谁connect并无严格要求(虽本质并无区别),但仍建议PUB使用bind,SUB使用connect。
zmq_setsockopt
int zmq_setsockopt (void *socket, int option_name, const void *option_value, size_t option_len);
zmq_setsockopt()函数应将设置option_name指定的选项,且使用option_value参数指定的值,对socket参数指定的ZMQ socket进行设置。option_len参数是选项值的字节数。
注意:除了以下属性,所有的属性均需要在对socket进行bind/connect操作之前设置: ZMQ_SUBSCRIBE, ZMQ_UNSUBSCRIBE, ZMQ_LINGER, ZMQ_ROUTER_HANDOVER, ZMQ_ROUTER_MANDATORY, ZMQ_PROBE_ROUTER, ZMQ_XPUB_VERBOSE, ZMQ_REQ_CORRELATE, and ZMQ_REQ_RELAXED 特别的,安全的属性也可以在bind/connect操作之后生效,并且可以随时进行修改并影响之后的bind/connect操作。
option_name有很多选项,下面介绍几个遇到的:
ZMQ_RCVTIMEO:在一个recv操作返回EAGAIN错误前的最大时间。
设置socket的接收操作超时时间。如果属性值是0,zmq_recv(3)函数将会立刻返回,如果没有接收到任何消息,将会返回EAGAIN错误。如果属性值是 -1,将会阻塞,直到接收到消息为止。对于任何其它值,都会进行等待这么多时间,直到返回EAGAIN错误。
ZMQ_SUBSCRIBE:创建消息过滤标志
ZMQ_SUBSCRIBE属性将会在ZMQ_SUB类型的socekt上创建一个新的消息过滤标志。新建立的ZMQ_SUB类型socket会对进入socket的所有消息进行过滤,这样你就可以使用这个属性来建立最初的消息过滤项。
一个option_value的长度是0的过滤属性会订阅所有的广播消息。一个非空的option_value值会只订阅所有以option_value的值为前缀的消息。一个ZMQ_SUB类型的socket可以附加多个过滤条件,只要一个消息符合过滤条件中的任何一个就会被接受。
zmq_close
int zmq_close (void *socket);//销毁由socket参数指定的socket
返回值:如果执行成功,zmq_close()函数会返回0。其它情况则返回-1,并且设置errno为下列值。
zmq_msg_init
int zmq_msg_init (zmq_msg_t *msg);
zmq_msg_init()函数会将msg参数引用的ZMQ消息对象进行初始化,使其成为一个空消息。在使用zmq_recv()函数接收消息之前调用此函数是很有必要的。
永远不要直接对zmq_msg_t对象进行直接操作,而是要使用zmq_msg函数族进行操作。
注意:zmq_msg_init()、zmq_msg_init_data()和zmq_msg_init_size()这三个函数是互斥的。永远不要把一个zmq_msg_t对象初始化两次。
zmq_msg_recv 从一个socket中接受一个消息帧
int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags);
zmq_msg_recv()函数将会从socket参数指定的的socket中读取消息帧,并存储在msg参数指定的ZMQ消息结构间中。以前存储在消息msg中的内容会被准确的释放。如果此刻,在socekt参数指定的的socket上没有消息可以接收,zmq_msg_recv()会进入阻塞状态,直到其请求被满足为止。flags参数是下列一些标志的组合。
返回值:如果zmq_msg_recv() 函数执行成功,会以字节为单位返回消息的大小。否则返回 -1,并且设置errno的值为下列指定的值。
zmq_msg_data 返回消息内容的指针
void *zmq_msg_data (zmq_msg_t *msg);//zmq_msg_data() 函数会返回msg参数指定的消息内容的指针
函数很多,不整理了。建议是查看官方API文档进行编程,基本编程思路是用zmq_socket之类的函数创建socket连接,然后用zmq_msg函数族进行消息的传递。
踩坑(针对zmq_msg_t消息进行处理的教训):
缘由:
按照Matrix中使用ZMQ时,是将CAN消息放入zmq_msg_t对象中,进行消息的收发以及对消息的处理。参照将消息放入zmq_msg_t中进行处理的逻辑,写了代码进行验证却发现Pub端能发送消息,Sub端却没法正确收到消息内容,甚至Pub端发送的内容长度为0,但是消息内容确实正确的奇怪现象,在此附上我的错误源码文件。
//Pub端
#include <iostream>
#include <string>
#include <zmq.h>
#include <unistd.h>
#include <cstring>//memcpy need this header-file
class MsgPub{
public:
MsgPub();
int MsgPubInit(std::string ip_port);
int MsgPubSend(int data);
void MsgPubDestory();
~MsgPub();
private:
void* zmq_context;
void* pub_socket;
};
MsgPub::MsgPub()
:zmq_context(nullptr),
pub_socket(nullptr)
{}
int MsgPub::MsgPubInit(std::string ip_port)
{
zmq_context = zmq_ctx_new();
pub_socket = zmq_socket(zmq_context, ZMQ_PUB);
if (pub_socket == nullptr) {
std::cout << "init sub socket failed " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
// int blocktime = 100;
// if (zmq_setsockopt(pub_socket, ZMQ_RCVTIMEO, &blocktime, sizeof(blocktime)) <
// 0) {
// std::cout << "set rev timeout info failed " << __FILE__ << ":" << __LINE__ << std::endl;
// return -1;
// }
std::string path = "tcp://" + ip_port;
if (zmq_bind(pub_socket, path.c_str()) < 0) {//client should connect,server should bind
std::cout << "bind failed: " << stderr << " " << path << " " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
// if (zmq_setsockopt(pub_socket, ZMQ_SUBSCRIBE, "", 0) < 0) { //set filter there is not any filter option
// std::cout << "set filter info failed " << __FILE__ << ":" << __LINE__ << std::endl;
// return -1;
// }
return 0;
}
int MsgPub::MsgPubSend(int data) {
zmq_msg_t msg;
if (zmq_msg_init(&msg) < 0) {
std::cout << "recv msg init failed " << __FILE__ << ":" << __LINE__ << std::endl;
zmq_msg_close(&msg);
return -1;
}
memcpy(zmq_msg_data(&msg), data, sizeof(data));
std::cout << "Send Msg Content: " << static_cast<int*>(zmq_msg_data(&msg)) << std::endl;
if (zmq_msg_send(&msg, pub_socket, 0) < 0) {
zmq_msg_close(&msg);
return -1;
}
zmq_msg_close(&msg);
return 0;
}
void MsgPub::MsgPubDestory() {
if (pub_socket) {
zmq_close(pub_socket);
pub_socket = nullptr;
}
if (zmq_context) {
zmq_ctx_destroy(zmq_context);
zmq_context = nullptr;
}
}
MsgPub::~MsgPub() { MsgPubDestory(); }
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "need two argc!!!" << std::endl;
return 1;
}
MsgPub msg_pub;
msg_pub.MsgPubInit(argv[1]);
int SendData = 123;
while(1)
{
msg_pub.MsgPubSend(SendData);
sleep(2);
}
msg_pub.MsgPubDestory();
return 0;
}
//Sub端
#include <iostream>
#include <string>
#include <zmq.h>
#include <cstring>//memcpy need this header-file
class MsgSub
{
public:
MsgSub();
int MsgSubInit(std::string ip_port);
int MsgSubRev(int& data);
void MsgSubDestory();
~MsgSub();
private:
void* zmq_context;
void* sub_socket;
};
MsgSub::MsgSub()
:zmq_context(nullptr),
sub_socket(nullptr)
{}
int MsgSub::MsgSubInit(std::string ip_port)
{
zmq_context = zmq_ctx_new();
sub_socket = zmq_socket(zmq_context, ZMQ_SUB);
if (sub_socket == nullptr) {
std::cout << "init sub socket failed " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
// if (zmq_setsockopt(sub_socket, ZMQ_RCVTIMEO, 0, 0) < 0) {
// std::cout << "set rev timeout info failed " << __FILE__ << ":" << __LINE__<< std::endl;
// return -1;
// }
int blocktime = 1000;
if (zmq_setsockopt(sub_socket, ZMQ_RCVTIMEO, &blocktime, sizeof(blocktime)) < 0) {
std::cerr << "set rev timeout info failed " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
std::string path = "tcp://" + ip_port;
if (zmq_connect(sub_socket, path.c_str()) < 0) {//client should connect,server should bind
std::cout << "connect failed: " << stdout << " " << path << " " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
if (zmq_setsockopt(sub_socket, ZMQ_SUBSCRIBE, "", 0) < 0) {
std::cout << "set filter info failed " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
return 0;
}
// int MsgSub::MsgSubRev(std::string& data){
// std::cout << "MsgSubRev come" << std::endl;
// zmq_msg_t msg;
// if (zmq_msg_init(&msg) < 0) {
// std::cout << "recv msg init failed " << __FILE__ << ":" << __LINE__ << std::endl;
// zmq_msg_close(&msg);
// return -1;
// }
// if (zmq_msg_recv(&msg, sub_socket, 0) < 0) {
// std::cout << "zmq_msg_recv" << std::endl;
// zmq_msg_close(&msg);
// return -1;
// }
// std::cout << "Rev Msg Content: " << static_cast<const char*>(zmq_msg_data(&msg)) << std::endl;
// data.assign(static_cast<const char*>(zmq_msg_data(&msg)), zmq_msg_size(&msg));
// zmq_msg_close(&msg);
// return 0;
// }
int MsgSub::MsgSubRev(int& data){
std::cout << "MsgSubRev come" << std::endl;
zmq_msg_t msg;
//while (true) {
if (zmq_msg_init(&msg) < 0) {
std::cout << "recv msg init failed " << __FILE__ << ":" << __LINE__ << std::endl;
zmq_msg_close(&msg);
return -1;
}
if (zmq_msg_recv(&msg, sub_socket, 0) < 0) {
std::cout << "zmq_msg_recv" << std::endl;
zmq_msg_close(&msg);
return -1;
//continue; // 继续等待下一条消息
}
std::cout << "Rev Msg Size: " << zmq_msg_size(&msg) << std::endl;
std::cout << "Rev Msg Content: " << reinterpret_cast<const char*>(zmq_msg_data(&msg)) << std::endl;
data.assign(reinterpret_cast<const char*>(zmq_msg_data(&msg)), zmq_msg_size(&msg));
zmq_msg_close(&msg);
return 0;
//}
}
void MsgSub::MsgSubDestory() {
if (sub_socket) {
zmq_close(sub_socket);
sub_socket = nullptr;
}
if (zmq_context) {
zmq_ctx_destroy(zmq_context);
zmq_context = nullptr;
}
}
MsgSub::~MsgSub() { MsgSubDestory(); }
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "need two argc!!!" << std::endl;
}
MsgSub msg_sub;
msg_sub.MsgSubInit(argv[1]);
int RevData;
while(1)
{
msg_sub.MsgSubRev(123);
std::cout << "sub receive msg: " << RevData << std::endl;
}
msg_sub.MsgSubDestory();
return 0;
}
如果仅仅使用zmq_send和zmq_recv函数进行处理(参照一开始的测试文件),而不是用zmq_msg_t对象配合着zmq_msg_send和zmq_msg_recv的时候并不会出现这类现象。
解决办法:
官网文档并没有对这一现象进行描述讲解,自己一步一步测试加参照优秀文档:
全网仅此一篇!万字详解ZeroMQ的zmq_msg_t消息处理、多部分消息、及消息接口_51CTO博客_zeromq消息队列
zmq_msg_init和zmq_msg_init_size()使用注意事项
在发送数据的时候:我们需要调用memcpy()将数据拷贝到zmq_msg_t中进行发送,不可以调用zmq_msg_init()初始化的zmq_msg_t对象进行存储,因为zmq_msg_init()初始化的对象其大小被设定为0,在调用zmq_msg_send()的时候会报错的。见下面代码:
//发送端
/**********这种初始化方式是错误的************/
zmq_msg_t msg;
if(zmq_msg_init(&msg))
{
printf("zmq_msg_init\n");
zmq_msg_close(&msg);
return -1;
}
/*************这种初始化才可以*************/
/*发送数据的时候请使用zmq_msg_init_size()初始化对象,
这样发送出去的zmq_msg_t对象是有大小的,
不会被zmq_msg_send()判断为是错的*/
char *str = "Hello";
zmq_msg_t msg;
zmq_msg_init_size(&msg, strlen(str) + 1);
memcpy(zmq_msg_data(&msg), str, strlen(str) + 1);
printf("Send size: %ld\n", zmq_msg_size(&msg));//这里能打印出发送消息的长度为6
//如果zmq_msg_init初始化,msg虽然有内容, 但是其大小为0
//如果zmq_msg_init_size初始化,msg有内容且调用成功,大小为0
zmq_msg_send(&msg, publisher, 0)
接收数据时,可以使用zmq_msg_init()定义的zmq_msg_t对象来保存数据,zmq_msg_recv()函数内部会自动的设置zmq_msg_t对象的大小
//接收端
// 初始化时指定其大小
zmq_msg_t msg;
zmq_msg_init(&msg);
// 接收数据, zmq_msg_recv()内部会自动
zmq_msg_recv(&msg, socket, 0);
还有一个重要的点在于如果Pub端循发消息的时候,那么需要把zmq_msg_init_size()的初始化放在循环内,不然Sub端依然无法收到消息。
下面是正确收发双方的源码:
// publisher.c
#include <zmq.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
void *context = zmq_ctx_new();
if (!context) {
printf("zmq_ctx_new\n");
return -1;
}
void *publisher = zmq_socket(context, ZMQ_PUB);
if (!publisher) {
printf("zmq_socket\n");
zmq_ctx_destroy(context);
return -1;
}
int drc = zmq_bind(publisher, "tcp://*:5556");
if (drc != 0) {
printf("zmq_bind\n");
zmq_close(publisher);
zmq_ctx_destroy(context);
return -1;
}
char *str = "Hello";
zmq_msg_t msg;
while (1) {
zmq_msg_init_size(&msg, strlen(str) + 1);
memcpy(zmq_msg_data(&msg), str, strlen(str) + 1);
printf("Send size: %ld\n", zmq_msg_size(&msg));
if(zmq_msg_send(&msg, publisher, 0) < 0)
{
zmq_msg_close(&msg);
return -1;
}
/*发完消息后打印Size的结果为0,真很奇怪,原因是:
当把msg传递给该函数之后,msg所对应的zmq_msg_t结构就失效了,zmq_msg_send()会
把zmq_msg_t对象的大小设置为0(变为0之后就标记这个zmq_msg_t对象不需要再去使用了),
但是没有关闭该对象,因此在zmq_msg_send()之后建议调用zmq_close()关闭msg参数
对应的zmq_msg_t对象。*/
printf("Send size: %ld\n", zmq_msg_size(&msg));
printf("Publisher: %s\n", (char *)zmq_msg_data(&msg));
sleep(2);
}
zmq_close(publisher);
zmq_ctx_destroy(context);
return 0;
}
// subscriber.c
#include <zmq.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
void *context = zmq_ctx_new();
if (!context) {
perror("zmq_ctx_new");
return -1;
}
void *subscriber = zmq_socket(context, ZMQ_SUB);
if (!subscriber) {
perror("zmq_socket");
zmq_ctx_destroy(context);
return -1;
}
int rc = zmq_connect(subscriber, "tcp://localhost:5556");
if (rc != 0) {
perror("zmq_connect");
zmq_close(subscriber);
zmq_ctx_destroy(context);
return -1;
}
rc = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, "", 0);
if (rc != 0) {
perror("zmq_setsockopt");
zmq_close(subscriber);
zmq_ctx_destroy(context);
return -1;
}
zmq_msg_t msg;
zmq_msg_init(&msg);
while (1) {
zmq_msg_recv(&msg, subscriber, 0);
int size = zmq_msg_size(&msg);
printf("recv size: %d\n", size);
char* string = (char*)malloc(size + 1);
memcpy(string, zmq_msg_data(&msg), size);
string[size] = '\0';
printf("Sub : %s\n", string);
}
zmq_msg_close(&msg);
zmq_close(subscriber);
zmq_ctx_destroy(context);
return 0;
}
发布/订阅(Pub/Sub)模式介绍
Matrix代码中涉及的是zmq发布订阅模式,所以系统学习一下这个模式。
关系:一个发布者、多个订阅者(1:N),当发布者数据变化时,所有订阅者均能接收到数据并处理。
订阅者调用zmq_send()来发送消息是会报错的,同样发布者使用zmq_recv()来接收消息也会报错(即发布订阅模式下消息传递是单向的,由发布者传向订阅者)。
-
发布者无法判断订阅者何时成功连接。
-
发布者发送消息的速度是全速的,订阅者必须要么跟上,要么丢失消息。
-
缺点是无法进行可靠的多播,但是优点是简单高效可拓展。
但是这种模式非常有用且广泛,就像你打开电视或者收音机,电视台属于发布者,而观众属于订阅者,二者之间消息单向传递。
上面我写的代码例子就是发布/订阅模式,下图是运行截图:
ref:中文版API网址https://www.cnblogs.com/fengbohello/p/4230135.html
英文版API网址ZMQ API reference