• ZeroMQ应用


    ZMQ是什么?

      这是个类似于Socket的一系列接口,他跟Socket的区别是:普通 的socket是端到端的(1:1的关系),而ZMQ却是可以N:M 的关系,人们对BSD套接字的了解较多的是点对点的连接,点对点连接需要显式地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而ZMQ屏 蔽了这些细节,让你的网络编程更为简单。ZMQ用于node与node间的通信,node可以是主机或者是进程。
      引用官方的说法: “ZMQ(以下ZeroMQ简称ZMQ)是一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是 “成为标准网络协议栈的一部分,之后进入Linux内核”。现在还未看到它们的成功。但是,它无疑是极具前景的、并且是人们更加需要的“传统”BSD套接 字之上的一 层封装。ZMQ让编写高性能网络应用程序极为简单和有趣。”
      以上拷贝至百度百科。
      对了使用ZMQ之前必须要有那么几样东西libzmq.lib,zhelpers.hpp,zmq.h,zmq.hpp。这些都可以在ZMQ的官网下载。
     
      接下来是来说说ZMQ有模式,可以归纳成三种,请求回应模式(1对N),发布订阅模式(单向1对N),还有推拉模型。
    1:请求回应模式(Req-Rep)
    可以有多个client,这个很容易理解跟TCP很像,但服务器与客户端必须是1问1答的形式。直接看源代码。
    #include <zmq.hpp>
    #include <string>
    #include <iostream>
    #include <windows.h>
    #include<zhelpers.hpp>
    using namespace std;
    
    DWORD WINAPI MyThread_client(LPVOID lpParamter)
    {
        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");
    
        for (int request_nbr = 0; request_nbr != 10; request_nbr++) {
            s_send (socket, "hello");
            std::cout << "Client1 Received :" <<s_recv (socket)<< std::endl;
    
            Sleep(1000);
        }
        return 0;
    
    }
    
    DWORD WINAPI MyThread_client1(LPVOID lpParamter)
    {
        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");
    
        for (int request_nbr = 0; request_nbr != 10; request_nbr++) {
            s_send (socket, "SB");
            std::cout << "Client2 Received :" <<s_recv (socket)<< std::endl;
    
            Sleep(1000);
        }
        return 0;
    
    }
    
    DWORD WINAPI MyThread_servce(LPVOID lpParamter)
    {
        zmq::context_t context (1);
        zmq::socket_t socket (context, ZMQ_REP);
        //绑定端口
        socket.bind ("tcp://*:5555");
     
        while (true) {
            std::cout << "Servce Received: "<<s_recv (socket)<< std::endl;
            s_send (socket, "world");
        }
    }
    
    int main () 
    {
    
        HANDLE hThread1 = CreateThread(NULL, 0, MyThread_client, NULL, 0, NULL);
        HANDLE hThread2 = CreateThread(NULL, 0, MyThread_servce, NULL, 0, NULL);
    
        HANDLE hThread3 = CreateThread(NULL, 0, MyThread_client1, NULL, 0, NULL);
    
        while(1);
        return 0;
    }

    这里我建立了两个客户端和一个服务器,每个都独立运行一个线程。客户端1发了“hello”,客户端2发了“SB”,服务器都能接收到并且返回了world。

    2:发布订阅模式(PUB-SUB)

    所谓发布订阅,比如天气预报,当很多人订阅之后,中心服务器直接往订阅的人发送就可以了,不需要管对方有没有收到。也就是1对N的模式。这里还有重要的一个概念,频道:跟收音机的频道类似,订阅者设定了频道才能听到该频道的消息

    #include <zmq.hpp>
    #include <string>
    #include <iostream>
    #include <windows.h>
    #include<zhelpers.hpp>
    using namespace std;
    
    
    //订阅1
    DWORD WINAPI MyThread_sub1(LPVOID lpParamter)
    {
        zmq::context_t context(1);
        zmq::socket_t subscriber (context, ZMQ_SUB);
        //连接
        subscriber.connect("tcp://localhost:5563");
        //设置频道B
        subscriber.setsockopt( ZMQ_SUBSCRIBE, "A", 1);
        while (1) {
     
            //  Read envelope with address
            std::string address = s_recv (subscriber);
            //  Read message contents
            std::string contents = s_recv (subscriber);
            
            std::cout << "订阅1:[" << address << "] " << contents << std::endl;
        }
        return 0;
    
    }
    //订阅2
    DWORD WINAPI MyThread_sub2(LPVOID lpParamter)
    {
        zmq::context_t context(1);
        zmq::socket_t subscriber (context, ZMQ_SUB);
        //连接
        subscriber.connect("tcp://localhost:5563");
        //设置频道B
        subscriber.setsockopt( ZMQ_SUBSCRIBE, "B", 1);
        while (1) {
     
            //  Read envelope with address
            std::string address = s_recv (subscriber);
            //  Read message contents
            std::string contents = s_recv (subscriber);
            
            std::cout << "订阅2:[" << address << "] " << contents << std::endl;
        }
        return 0;
    
    }
    //订阅3
    DWORD WINAPI MyThread_sub3(LPVOID lpParamter)
    {
        zmq::context_t context(1);
        zmq::socket_t subscriber (context, ZMQ_SUB);
        //连接
        subscriber.connect("tcp://localhost:5563");
        //设置频道B
       // subscriber.setsockopt( ZMQ_SUBSCRIBE, "B", 1);
        while (1) {
     
            //  Read envelope with address
            std::string address = s_recv (subscriber);
            //  Read message contents
            std::string contents = s_recv (subscriber);
            
            std::cout << "订阅3:[" << address << "] " << contents << std::endl;
        }
        return 0;
    
    }
    //发布线程
    DWORD WINAPI MyThread_pub(LPVOID lpParamter)
    {
        //  Prepare our context and publisher
        zmq::context_t context(1);
        zmq::socket_t publisher(context, ZMQ_PUB);
        publisher.bind("tcp://*:5563");
    
        while (1) {
            //  Write two messages, each with an envelope and content
            s_sendmore (publisher, "A");
            s_send (publisher, "We don't want to see this");
    
            Sleep (100);
            s_sendmore (publisher, "B");
            s_send (publisher, "We would like to see this");
            Sleep (100);
    
        }
    }
    
    int main () 
    {
        HANDLE hThread1 = CreateThread(NULL, 0, MyThread_pub, NULL, 0, NULL);
        Sleep(1000);
        HANDLE hThread2 = CreateThread(NULL, 0, MyThread_sub1, NULL, 0, NULL);
        HANDLE hThread3 = CreateThread(NULL, 0, MyThread_sub2, NULL, 0, NULL);
        HANDLE hThread4 = CreateThread(NULL, 0, MyThread_sub3, NULL, 0, NULL);
        while(1);
        return 0;
    }

    例程中,设置了1个发布端和3个订阅端,订阅端订阅的频道分别是是A,B和没有订阅,发布端发布了对应频道的订阅消息。由此订阅3没有设置频道,所以收不到消息。
     
    3:推拉模式,还没研究过,不过看网上说的,跟发布订阅模式类似,只是可以负载均衡。目前项目中也没有用到,下次有机会再研究吧。
    ZEROMQ官网的github上面有详细的例程:https://github.com/imatix/zguide/tree/master/examples/
     

    ZMQ API

    ZMQ提供的所有API均以zmq_开头,

    #include <zmq.h>
    
    gcc [flags] files -lzmq [libraries]

    例如,返回当前ZMQ库的版本信息

    void zmq_version (int *major, int *minor, int *patch);

    Context

    在使用任何ZQM库函数之前,必须首先创建ZMQ context(上下文),程序终止时,也需要销毁context。


    创建context

    void *zmq_ctx_new ();

    ZMQ context是线程安全的,可以在多线程环境使用,而不需要程序员对其加/解锁。

    在一个进程中,可以有多个ZMQ context并存。

    设置context选项

    int zmq_ctx_set (void *context, int option_name, int option_value);
    int zmq_ctx_get (void *context, int option_name);

    销毁context

    int zmq_ctx_term (void *context);

    Sockets

    ZMQ Sockets 是代表异步消息队列的一个抽象,注意,这里的ZMQ socket和POSIX套接字的socket不是一回事,ZMQ封装了物理连接的底层细节,对用户不透明。

    传统的POSIX套接字只能支持1对1的连接,而ZMQ socket支持多个Client的并发连接,甚至在没有任何对端(peer)的情况下,ZMQ sockets上也能放入消息;

    ZMQ sockets不是线程安全的,因此,不要在多个线程中并行操作同一个sockets。

    创建ZMQ  Sockets

    void *zmq_socket (void *context, int type);

    注意,ZMQ socket在bind之前还不能使用。

    type参数含义

    pattern

    type

    description

    一对一结对模型

    ZMQ_PAIR

    请求回应模型

    ZMQ_REQ

    client端使用

    ZMQ_REP

    server端使用

    ZMQ_DEALER

    将消息以轮询的方式分发给所有对端(peers)

    ZMQ_ROUTER

    发布订阅模型

    ZMQ_PUB

    publisher端使用

    ZMQ_XPUB

    ZMQ_SUB

    subscriber端使用

    ZMQ_XSUB

    管道模型

    ZMQ_PUSH

    push端使用

    ZMQ_PULL

    pull端使用

    原生模型

    ZMQ_STREAM

    设置socket选项

    int zmq_getsockopt (void *socket, int option_name, void *option_value, size_t *option_len);
    int zmq_setsockopt (void *socket, int option_name, const void *option_value, size_t option_len);

    关闭socket

    int zmq_close (void *socket);

    创建一个消息流

    int zmq_bind (void *socket, const char *endpoint);
    int zmq_connect (void *socket, const char *endpoint);

    bind函数是将socket绑定到本地的端点(endpoint),而connect函数连接到指定的peer端点。

    endpoint支持的类型:

    transports

    description

    uri example

    zmp_tcp

    TCP的单播通信

    tcp://*:8080

    zmp_ipc

    本地进程间通信

    ipc://

    zmp_inproc

    本地线程间通信

    inproc://

    zmp_pgm

    PGM广播通信

    pgm://

    收发消息

    int zmq_send (void *socket, void *buf, size_t len, int flags);
    int zmq_recv (void *socket, void *buf, size_t len, int flags);
    int zmq_send_const (void *socket, void *buf, size_t len, int flags);

    zmq_recv()函数的len参数指定接收buf的最大长度,超出部分会被截断,函数返回的值是接收到的字节数,返回-1表示出错;

    zmq_send()函数将指定buf的指定长度len的字节写入队列,函数返回值是发送的字节数,返回-1表示出错;

    zmq_send_const()函数表示发送的buf是一个常量内存区(constant-memory),这块内存不需要复制、释放。

    socket事件监控

    int zmq_socket_monitor (void *socket, char * *addr, int events);

    zmq_socket_monitor()函数会生成一对sockets,publishers端通过inproc://协议发布 sockets状态改变的events;
    消息包含2帧,第1帧包含events id和关联值,第2帧表示受影响的endpoint。

    监控支持的events:

    ZMQ_EVENT_CONNECTED: 建立连接
    ZMQ_EVENT_CONNECT_DELAYED: 连接失败
    ZMQ_EVENT_CONNECT_RETRIED: 异步连接/重连
    ZMQ_EVENT_LISTENING: bind到端点
    ZMQ_EVENT_BIND_FAILED: bind失败
    ZMQ_EVENT_ACCEPTED: 接收请求
    ZMQ_EVENT_ACCEPT_FAILED: 接收请求失败
    ZMQ_EVENT_CLOSED: 关闭连接
    ZMQ_EVENT_CLOSE_FAILED: 关闭连接失败
    ZMQ_EVENT_DISCONNECTED: 会话(tcp/ipc)中断

    I/O多路复用

    int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout);

    对sockets集合的I/O多路复用,使用水平触发。

    与epoll类似,items参数指定一个结构体数组(结构体定义如下),nitems指定数组的元素个数,timeout参数是超时时间(单位:ms,0表示不等待立即返回,-1表示阻塞等待)。

     
    typedef struct
    {
        void *socket;
        int fd;
        short events;
        short revents;
    } zmq_pollitem_t;
     

    对于每个zmq_pollitem_t元素,ZMQ会同时检查其socket(ZMQ套接字)和fd(原生套接字)上是否有指定的events发生,且ZMQ套接字优先。

    events指定该sockets需要关注的事件,revents返回该sockets已发生的事件,它们的取值为:

    • ZMQ_POLLIN,可读;
    • ZMQ_POLLOUT,可写;
    • ZMQ_POLLERR,出错;

    Messages

    一个ZMQ消息就是一个用于在消息队列(进程内部或跨进程)中进行传输的数据单元,ZMQ消息本身没有数据结构,因此支持任意类型的数据,这完全依赖于程序员如何定义消息的数据结构。

    一条ZMQ消息可以包含多个消息片(multi-part messages),每个消息片都是一个独立zmq_msg_t结构。

    ZMQ保证以原子方式传递消息,要么所有消息片都发送成功,要么都不成功。

    初始化消息

    typedef void (zmq_free_fn) (void *data, void *hint);
    int zmq_msg_init (zmq_msg_t *msg);
    int zmq_msg_init_data (zmq_msg_t *msg, void *data, size_t size, zmq_free_fn *ffn, void *hint);
    int zmq_msg_init_size (zmq_msg_t *msg, size_t size);

    zmq_msg_init()函数初始化一个消息对象zmq_msg_t ,不要直接访问zmq_msg_t对象,可以通过zmq_msg_* 函数来访问它。
    zmq_msg_init()、zmq_msg_init_data()、zmq_msg_init_size() 三个函数是互斥的,每次使用其中一个即可。

    设置消息属性

    int zmq_msg_get (zmq_msg_t *message, int property);
    int zmq_msg_set (zmq_msg_t *message, int property, int value);


    释放消息

    int zmq_msg_close (zmq_msg_t *msg);


    收发消息

    int zmq_msg_send (zmq_msg_t *msg, void *socket, int flags);
    int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags);

     其中,flags参数如下:

    ZMQ_DONTWAIT,非阻塞模式,如果没有可用的消息,将errno设置为EAGAIN;
    ZMQ_SNDMORE,发送multi-part messages时,除了最后一个消息片外,其它每个消息片都必须使用 ZMQ_SNDMORE 标记位。

    获取消息内容

    void *zmq_msg_data (zmq_msg_t *msg);
    int zmq_msg_more (zmq_msg_t *message);
    size_t zmq_msg_size (zmq_msg_t *msg);

    zmq_msg_data()返回指向消息对象所带内容的指针;
    zmq_msg_size()返回消息的字节数;
    zmq_msg_more()标识该消息片是否是整个消息的一部分,是否还有更多的消息片待接收;


    控制消息

    int zmq_msg_copy (zmq_msg_t *dest, zmq_msg_t *src);
    int zmq_msg_move (zmq_msg_t *dest, zmq_msg_t *src);

    zmq_msg_copy()函数实现的是浅拷贝;
    zmq_msg_move()函数中,将dst指向src消息,然后src被置空。

    eg,接收消息的代码示例:

     
    zmq_msg_t part;
    while (true) {
        //  Create an empty ØMQ message to hold the message part
        int rc = zmq_msg_init (&part);
        assert (rc == 0); 
        //  Block until a message is available to be received from socket
        rc = zmq_msg_recv (socket, &part, 0); 
        assert (rc != -1);
        if (zmq_msg_more (&part))
            fprintf (stderr, "more
    ");
        else {
            fprintf (stderr, "end
    ");
            break;
        }   
        zmq_msg_close (&part); 
    }
     

    代理

    ZMQ提供代理功能,代理可以在前端socket和后端socket之间转发消息。

    int zmq_proxy (const void *frontend, const void *backend, const void *capture);
    int zmq_proxy_steerable (const void *frontend, const void *backend, const void *capture, const void *control);

    共享队列(shared queue),前端是ZMQ_ROUTER socket,后端是ZMQ_DEALER socket,proxy会把clients发来的请求,公平地分发给services;
    转发队列(forwarded),前端是ZMQ_XSUB socket, 后端是ZMQ_XPUB socket, proxy会把从publishers收到的消息转发给所有的subscribers;
    流(streamer),前端是ZMQ_PULL socket, 后端是ZMQ_PUSH socket.

    proxy使用的一个示例:

     
    //  Create frontend and backend sockets
    void *frontend = zmq_socket (context, ZMQ_ROUTER);
    assert (backend);
    void *backend = zmq_socket (context, ZMQ_DEALER);
    assert (frontend);
    
    //  Bind both sockets to TCP ports
    assert (zmq_bind (frontend, "tcp://*:5555") == 0);
    assert (zmq_bind (backend, "tcp://*:5556") == 0);
    
    //  Start the queue proxy, which runs until ETERM zmq_proxy frontend, backend, NULL);
     


    错误处理

    ZMQ库使用POSIX处理函数错误,返回NULL指针或者负数时表示调用出错。

    int zmq_errno (void);
    const char *zmq_strerror (int errnum);

    zmq_errno()函数返回当前线程的错误码errno变量的值;

    zmq_strerror()函数将错误映射成错误字符串。

    加密传输

    ZQM可以为IPC和TCP连接提供安全机制:

    • 不加密,zmq_null
    • 使用用户名/密码授权,zmq_plain
    • 椭圆加密,zmq_curve

    这些通过 zmq_setsockopt()函数设置socket选项的时候配置。

  • 相关阅读:
    设计模式相关,单例模式!
    混合App交互相关,jsBridge的原理
    Vue相关,插槽怎么用!
    Mysql相关,查询功能整理
    图文并茂讲解进程与线程
    线程与进程的区别及其通信方式
    算法,十进制数转任意进制数
    JavaScript,leetcode第198题,打家劫舍
    2.4G无线控制器附加AT2401C功放IC增加距离
    超低功耗WiFi :ESP8089
  • 原文地址:https://www.cnblogs.com/leijiangtao/p/8707372.html
Copyright © 2020-2023  润新知