ZMQ第三方通讯库的编译与使用

ZMQ简介

  ZeroMQ(简称ZMQ)是一个基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。ZMQ是网络通信中新的一层,介于应用层和传输层之间(按照TCP/IP划分),其是一个可伸缩层,可并行运行,分散在分布式系统间。ZMQ不是单独的服务,而是一个嵌入式库,它封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。

ZMQ源码编译(Windows平台)

环境介绍

  • 操作系统:Windows10专业版(1909)
  • 编译工具:Microsoft Visual Studio 2017专业版
  • 源码版本:zmq4.2.5
  • 编辑器:VS Code

源码下载

  可以在github的仓库中找到相对应的源码进行下载。如果下载速度较慢,可以转存到coding或者gitee上然后再下载。可以使用git clone命令或者直接下载zip压缩包的方式下载到源码,这里我选择了4.2.5版本。
  如果使用git clone命令,则默认应在libzmq文件夹下;如果采用直接下载压缩包的方式,则解压后一般带有版本号。需要保证存放的文件夹路径为libzmq,否则后面编译时会报错找不到某些文件。

编译过程

打开Visual Studio工程文件

  在libzmq/builds/msvc/vs2017文件夹中找到.sln后缀的工程文件,使用Microsoft Visual Studio打开,如果提示sdk版本匹配更新的问题,请同意更新。
  如下图示:

  如果提示sdk版本匹配更新时,没有选择同意更新,导致Windows的sdk不匹配报错,则可以在Solution菜单上使用右键选择Retarget solution选项,然后选择电脑上已安装的sdk版本完成更新即可。

选择要编译生成的版本

  在工具栏选择要生成的版本,然后使用Build菜单生成即可。比如我这里的DynDebugx64选项,表示生成64位包含debug信息的动态库。根据需要选择编译生成相应的版本即可,每个版本都会分开存放。

生成文件说明

  生成的库文件存放在libzmq文件夹中的bin目录下,而且按照不同的版本分文件夹存放,可以复制相应的头文件和库文件到需要的地方以备使用。

cppzmq

  cppzmq包含了专门用于C++版本的头文件。如果需要用到,可以直接去cppzmq仓库中下载两个.hpp后缀的文件放在需要使用的位置。

发布者与订阅者模式

  接下来,编写一个简单的发布-订阅模式的demo,以供学习使用。ZMQ的使用非常简单,具有固定的套路,只需要根据官方提供的API编写相应的操作即可,这里仅仅给出非常简单的发布者和订阅者模式的demo。
  其文件基本结构如下所示:

.
|-- example1
|   |-- publisher.cpp               //发布者节点
|   |-- subscriber.cpp              //订阅者节点
|   |-- CMakeLists.txt              //CMakeLists.txt文件
|   `-- build                       //build目录
|       `-- CMakeFiles
|           |-- 3.21.0-rc1
|           |   |-- CompilerIdC
|           |   |   `-- tmp
|           |   `-- CompilerIdCXX
|           |       `-- tmp
|           |-- CMakeTmp
|           |-- publisher.dir
|           `-- subscriber.dir
`-- tool                            //zmq的第三方库,使用tool文件夹统一放置其他库文件
    `-- zmq425
        |-- bin
        |   `-- x64
        |       |-- Debug
        |       |   `-- v141
        |       |       |-- dynamic//动态库文件
        |       |       `-- static
        |       `-- Release
        |           `-- v141
        |               |-- dynamic
        |               `-- static
        `-- include                //头文件路径
            `-- cpp                //cppzmq仓库中的头文件路径文件夹

基本概念

  ZMQ的一个节点需要先定义一个上下文(context,可以理解为句柄,或者该节点的总管家,用于维护该节点的相关资源),然后定义一个套接字(socket,用于绑定到某个地址上用于通讯)。很多API都会有返回值,而且很多代码都会在接收到返回值之后,使用assert编写断言,这个作用是为了确保API的调用符合预期,否则会输出报错提示,方便我们定位错误。ZMQ数据的发送以字节流的方式进行,所以对于字符串而言,并不关心最后一位的字符串结尾标记位,需要程序员自行处理。
  以上说明,仅为个人理解,可能并不严谨,仅供参考。

编写发布者

  编写发布者,通过提示使用者输入一串字符串,将使用者输入的字符串作为消息发送到一个固定的地址上,并对每一次发送的消息通过打印的方式提示使用者已经完成发送。代码实现如下:

#include<cpp/zmq.hpp>                               //c++的头文件专门用了一个单独的文件夹存放
#include<iostream>
#include<string>
int main(void){
    using namespace std;
    const string address="tcp://127.0.0.1:8888";    //发布地址
    void* context=zmq_ctx_new();                    //创建上下文,名称可以自定义,就是个变量
    void* socket=zmq_socket(context,ZMQ_PUB);       //创建套接字,名称可以自定义,就是个变量,类型为ZMQ_PUB表示为发布者
    int rc=zmq_bind(socket,address.c_str()); assert(rc==0); //绑定套接字到tcp协议的某个端口上,并对返回值进行断言判定
    int i=1;                                        //计数器
    void* buffer=malloc(256);                       //创建buffer,用于存放消息,假定最长不超过256个字符
    while(true){
        cout<<"Please enter your message:"<<endl;   //提示输入
        string msg_string;
        getline(cin,msg_string);                    //用于接收整行的消息
        if(msg_string=="end") break;                //如果接收到end字符串则停止接收消息,否则一直接收
        int size=msg_string.size();                 //将接收到的字符串复制到buffer中
        char *temp=(char*)buffer;
        for(int i=0;i<size;i++)
            temp[i]=msg_string[i];
        rc=zmq_send(socket,buffer,size,0); assert(rc==size);    //返回值为发送的数据字节数,使用send将buffer中的数据发送出去,需要指定发送的数据长度
        cout<<"Send NO."<<i++<<" message: "<<msg_string<<endl;  //提示消息已经发出
        Sleep(1);                                   //稍作停顿
    }
    zmq_close(socket);                              //结束后关闭套接字和上下文环境
    zmq_ctx_shutdown(context);
    return 0;
}

编写订阅者

  编写订阅者,订阅者在发布者发布的端口上接收消息,然后转换为字符串,并输出接收到的消息。代码实现如下:

#include<cpp/zmq.hpp>
#include<iostream>
int main(void){
    using namespace std;
    const string address="tcp://127.0.0.1:8888";                //确定接收地址
    void* context=zmq_ctx_new();                                //创建上下文
    void* socket=zmq_socket(context,ZMQ_SUB);                   //创建套接字,类型为ZMQ_SUB表示为订阅者
    int rc=zmq_connect(socket,address.c_str()); assert(rc==0);  //链接到发布者发布的地址上,并对返回值进行断言判定
    rc = zmq_setsockopt (socket, ZMQ_SUBSCRIBE, "", 0); assert(rc==0);//设置订阅该端口上的所有信息,没有这一句,则收不到消息,可以使用过滤器过滤接收
    int i=1;                                                    //计数器
    void* buffer=malloc(256);                                   //创建buffer,用于接收消息
    while(true){
        rc=zmq_recv(socket,buffer,256,0); assert(rc!=-1);       //接收一条消息
        int size=rc;
        char *rec_msg=(char*)malloc(sizeof(size+1));            //创建一个字符数组,用于存放消息
        memcpy(rec_msg,buffer,size); rec_msg[size]='\0';        //构建字符串
        cout<<"Receive NO."<<i++<<" message: "<<rec_msg<<endl;  //输出消息
        free(rec_msg);                                          //释放字符数字空间
    }
    zmq_close(socket);                                          //由于循环不会结束,所以以下两句并不会执行
    zmq_ctx_shutdown(context);                                  //但是依然要有回收清理的收尾习惯
    return 0;
}

编写CMakeLists.txt文件

  使用cmake+MinGW编译整个demo,因此需要编写CMakeLists.txt文件,内容如下:

# 版本和项目名称
cmake_minimum_required(VERSION 3.20)
project(example1)
# 设定编译标准为C++11
# 设定头文件路径和动态库路径
set(CXX_STANDARD 11)
set(INCLUDE_DIR "../tool/zmq425/include")
set(LIB_DIR "../tool/zmq425/bin/x64/Debug/v141/dynamic")
INCLUDE_DIRECTORIES(${INCLUDE_DIR})
LINK_DIRECTORIES(${LIB_DIR})
LINK_LIBRARIES(libzmq.lib)
# 生成可执行文件并连接到库文件
add_executable(publisher publisher.cpp)
add_executable(subscriber subscriber.cpp)
target_link_libraries(publisher libzmq.lib)
target_link_libraries(subscriber libzmq.lib)

编译

  在项目文件夹的build目录下使用以下命令进行编译:

  • cmake
    # 命令
    cmake .. -G "MinGW Makefiles"
    # 执行结果显示
    -- The C compiler identification is GNU 8.1.0
    -- The CXX compiler identification is GNU 8.1.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: D:/MinGW/mingw/mingw64/bin/gcc.exe - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: D:/MinGW/mingw/mingw64/bin/g++.exe - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: F:/code/learnZMQ/learncode/example1/build
  • make
    # 命令
    mingw32-make
    # 执行结果显示
    [ 25%] Building CXX object CMakeFiles/publisher.dir/publisher.cpp.obj
    [ 50%] Linking CXX executable publisher.exe
    [ 50%] Built target publisher
    [ 75%] Building CXX object CMakeFiles/subscriber.dir/subscriber.cpp.obj
    [100%] Linking CXX executable subscriber.exe
    [100%] Built target subscriber

测试

  因为需要用到动态库,需要先把动态库.dll文件的路径添加到系统变量中去,才能保证可执行文件的正常运行,否则会报错找不到.dll文件。
  打开build目录中生成的可执行文件,在发布者窗口中发布消息,则能够看到订阅者可以正常订阅到消息。


当珍惜每一片时光~