ØMQ的简单使用

简介

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提供了三种基本的通信方式:

  1. 请求-回复模式(Request-Reply)

  2. 发布-订阅模式(Publisher-Subscriber)

  3. 管道模式(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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值