ZeroMQ使用指南,基于消息队列的多线程网络库

开始

确保已经下载libzmq和cppzmq。快速安装教程:https://blog.csdn.net/lianshaohua/article/details/109384444
以下的代码编译,可以参考 编译指南
章节 zmq的通讯模式

示例一 hello world

进程间通信示例,进程有主线程和 receiver 线程,主线程发送"hello world"给 receiver 线程。

#include <zmq.hpp>
#include <iostream>
#include <thread>

void receiver(zmq::context_t& ctx) {                               // 接收共享的上下文引用(共用一个上下文 zmq::context_t)
    zmq::socket_t sock(ctx, zmq::socket_type::pull);               // zmq::socket_type::pull 表示接收方
    sock.connect("inproc://test");                                 // 连接 "inproc://test" 进程
    
    zmq::message_t msg;                                            // zmq的消息格式
    auto result = sock.recv(msg, zmq::recv_flags::none);           // 阻塞,会等待接收到 msg
    if (result) {
        std::cout << "Received: " << msg.to_string() << std::endl;
    }
}

int main() {
    zmq::context_t ctx;                                            // 在主线程创建唯一上下文
    
    std::thread recv_thread(receiver, std::ref(ctx));              // receiver 线程,给 receiver 传递上下文引用
    
    zmq::socket_t sock(ctx, zmq::socket_type::push);               // zmq::socket_type::push 表示发送方
    sock.bind("inproc://test");
    
    std::this_thread::sleep_for(std::chrono::milliseconds(100));   // 这里发送方socket已经连接,给 recv_thread 线程预留时间连接"inproc://test"进程
    
    sock.send(zmq::str_buffer("Hello, world"), zmq::send_flags::dontwait);  // 发送 "hello world"
    
    recv_thread.join();                                            // 等待 recv_thread 线程结束
    return 0;
}

执行结果:

> Received: Hello, world

示例二 multipart_messages

send_multipart 的使用,发送多部分消息。

#include <iostream>
#include <zmq_addon.hpp>

int main()
{
    zmq::context_t ctx;
    zmq::socket_t sock1(ctx, zmq::socket_type::push);                           // 发送方 socket
    zmq::socket_t sock2(ctx, zmq::socket_type::pull);                           // 接收方 socket
    sock1.bind("tcp://127.0.0.1:*");                                            // 系统分配一个本地端口 发送方 bind
    const std::string last_endpoint = sock1.get(zmq::sockopt::last_endpoint);   // 本地端口字符串
    std::cout << "Connecting to " << last_endpoint << std::endl;
    sock2.connect(last_endpoint);                                               // 接收方 connect

    std::array<zmq::const_buffer, 2> send_msgs = {
        zmq::str_buffer("header"),                                              // 消息数组
        zmq::str_buffer("content")
    };
    if (!zmq::send_multipart(sock1, send_msgs))                                 // 发送作为原子操作 要么都被接受要么都不被接收
        return 1;

    std::vector<zmq::message_t> recv_msgs;
    const auto ret = zmq::recv_multipart(
        sock2, std::back_inserter(recv_msgs)                                    // std::back_inserter 制作一个 recv_msgs 的 push_back 迭代器
        );
    if (!ret)
        return 1;
    std::cout << "Got " << *ret << " messages" << std::endl;                    // *ret 是接收消息个数
    for(int i = 0; i<*ret; i++) {
        std::cout << recv_msgs[i].to_string() << std::endl;
    }
    return 0;
}

执行结果:

> Connecting to tcp://127.0.0.1:33583
> Got 2 messages
> header
> content

示例三 pubsub_multithread_inproc

发布-订阅模式。消息信封功能。

#include <future>
#include <iostream>
#include <string>
#include <thread>

#include <zmq.hpp>
#include <zmq_addon.hpp>

void PublisherThread(zmq::context_t *ctx) {
    //  Prepare publisher
    zmq::socket_t publisher(*ctx, zmq::socket_type::pub);                  // zmq::socket_type::pub 声明一个发布者 socket
    publisher.bind("inproc://#1");

    // Give the subscribers a chance to connect, so they don't lose any messages
    std::this_thread::sleep_for(std::chrono::milliseconds(20));            // 这里 publisher 已经连接到进程,确保 subscriber 有足够时间连接,避免消息丢失

    while (true) {
        //  Write three messages, each with an envelope and content
        publisher.send(zmq::str_buffer("A"), zmq::send_flags::sndmore);     // zmq::send_flags::sndmore 表示消息发送并未结束
        publisher.send(zmq::str_buffer("Message in A envelope"));
        publisher.send(zmq::str_buffer("B"), zmq::send_flags::sndmore);
        publisher.send(zmq::str_buffer("Message in B envelope"));
        publisher.send(zmq::str_buffer("C"), zmq::send_flags::sndmore);
        publisher.send(zmq::str_buffer("Message in C envelope"));
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void SubscriberThread1(zmq::context_t *ctx) {
    //  Prepare subscriber
    zmq::socket_t subscriber(*ctx, zmq::socket_type::sub);                  // zmq::socket_type::sub 声明一个订阅者 socket 
    subscriber.connect("inproc://#1");

    //  Thread2 opens "A" and "B" envelopes
    subscriber.set(zmq::sockopt::subscribe, "A");                           // 订阅 "A" 
    subscriber.set(zmq::sockopt::subscribe, "B");

    while (1) {
        // Receive all parts of the message
        std::vector<zmq::message_t> recv_msgs;
        zmq::recv_result_t result = zmq::recv_multipart(subscriber, std::back_inserter(recv_msgs));
        assert(result && "recv failed");
        assert(*result == 2);

        std::cout << "Thread2: [" << recv_msgs[0].to_string() << "] "
                  << recv_msgs[1].to_string() << std::endl;
    }
}

void SubscriberThread2(zmq::context_t *ctx) {
    //  Prepare our context and subscriber
    zmq::socket_t subscriber(*ctx, zmq::socket_type::sub);
    subscriber.connect("inproc://#1");

    //  Thread3 opens ALL envelopes
    subscriber.set(zmq::sockopt::subscribe, "");

    while (1) {
        // Receive all parts of the message
        std::vector<zmq::message_t> recv_msgs;
        zmq::recv_result_t result = zmq::recv_multipart(subscriber, std::back_inserter(recv_msgs));
        assert(result && "recv failed");
        assert(*result == 2);

        std::cout << "Thread3: [" << recv_msgs[0].to_string() << "] "
                  << recv_msgs[1].to_string() << std::endl;
    }
}

int main() {
    /*
     * No I/O threads are involved in passing messages using the inproc transport.
     * Therefore, if you are using a ØMQ context for in-process messaging only you
     * can initialise the context with zero I/O threads.
     *
     * Source: http://api.zeromq.org/4-3:zmq-inproc
     */
    zmq::context_t ctx(0);         // 进程内消息传递,那么可以把 I/O线程个数设置为 0

    auto thread1 = std::async(std::launch::async, PublisherThread, &ctx);   // std::async() 创建并启动异步线程调用PublisherThread,立马返回

    std::this_thread::sleep_for(std::chrono::milliseconds(10));             // PublisherThread 需要时间 bind 到进程,在这期间暂不启动订阅者进程 SubscriberThread

    auto thread2 = std::async(std::launch::async, SubscriberThread1, &ctx);
    auto thread3 = std::async(std::launch::async, SubscriberThread2, &ctx);
    thread1.wait();                                                         // 等待 thread1 结束,thread1 是无限循环,因此不会自动结束
    thread2.wait();
    thread3.wait();

    /*
     * Output:
     *   An infinite loop of a mix of:
     *     Thread2: [A] Message in A envelope
     *     Thread2: [B] Message in B envelope
     *     Thread3: [A] Message in A envelope
     *     Thread3: [B] Message in B envelope
     *     Thread3: [C] Message in C envelope
     */
}

示例四

在本地端口实现发布-订阅模式

发布者代码

#include <zmq.hpp>
#include <iostream>
#include <sstream>
#include <thread>
int main() {
    zmq::context_t context(1);
    zmq::socket_t publisher(context, ZMQ_PUB);    // 宏 ZMQ_PUB 等效于 zmq::socket_type::pub
    publisher.bind("tcp://*:6666");
 
    while (true) {
        std::ostringstream message;
        message << "Hello, World!";
        publisher.send(zmq::buffer(message.str()), zmq::send_flags::none);
        std::cout << "Sent: " << message.str() << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

订阅者代码

#include <zmq.hpp>
#include <iostream>
 
int main() {
    zmq::context_t context(1);
    zmq::socket_t subscriber(context, ZMQ_SUB);   // 宏 ZMQ_SUB 等效于 zmq::socket_type::sub
    subscriber.connect("tcp://localhost:6666");
    subscriber.set(zmq::sockopt::subscribe, "");
 
    while (true) {
        zmq::message_t message;
        auto result = subscriber.recv(message, zmq::recv_flags::none);
        if (result.has_value()) {
            std::cout << "Received: " << message.to_string() << std::endl;
        }
    }
    return 0;
}

示例五

来实现一个交互式消息发送,接收方订阅发送给 "10086" 的消息:

发布者:

#include <zmq.hpp>
#include <iostream>
#include <sstream>
#include <thread>
int main() {
    zmq::context_t context(1);
    zmq::socket_t publisher(context, ZMQ_PUB);
    publisher.bind("tcp://*:6666");
 
    while (true) {
        std::ostringstream message;
        std::string to;
        std::string content;
        std::cout << "to>";
        std::cin >> to;
        int ch;
        while ((ch = getchar()) != '\n' && ch != EOF);                 // 输入完 to> 的内容,清空清空缓冲区内容
        std::cout << "content>";
        std::getline(std::cin, content);                               // 整行读取
        std::cout << std::endl;
        publisher.send(zmq::buffer(to), zmq::send_flags::sndmore);     // 作为目的编号
        publisher.send(zmq::buffer(content), zmq::send_flags::none);   // 作为内容
    }
    return 0;
}

订阅者:

#include <zmq.hpp>
#include <zmq_addon.hpp>
#include <iostream>
#include <vector>
int main() {
    zmq::context_t context(1);
    zmq::socket_t subscriber(context, ZMQ_SUB);
    subscriber.connect("tcp://localhost:6666");
    subscriber.set(zmq::sockopt::subscribe, "10086");     // 订阅 10086 的消息
 
    while (true) {
        std::vector<zmq::message_t> messages;
        auto result = zmq::recv_multipart(subscriber, std::back_inserter(messages));
        assert(result && "recv failed");
        assert(*result == 2);

        if (*result == 2 && result.has_value()) {
            std::cout << "<Received> To "<< messages[0].to_string() << ":" << messages[1].to_string() << std::endl;
        }
    }
    return 0;
}

Run Output:

# ./bin/Publish  # 启动 Publish 程序
to>123
content>你好,123

to>10086
content>你好,10086

# ./bin/Receive  # 只会收到它订阅的内容
<Received> To 10086:你好,10086

编译指南

使用 cmake3.11 编译 C++ 代码。

# CMakeLists.txt 文件内容
cmake_minimum_required(VERSION 3.11 FATAL_ERROR)

project(cppzmq-examples CXX)

# place binaries and libraries according to GNU standards

include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

set(CMAKE_CXX_STANDARD 11)                  # 用 c++11 标准,版本最兼容
set(CMAKE_CXX_STANDARD_REQUIRED ON)         # c++11 ON (zmq.hpp、zmq_addon.hpp 仍出现一些编译问题,我是通过修改发布的hpp文件解决的)

find_package(Threads)
find_package(cppzmq)

add_executable(
    hello_world
    hello_world.cpp
)
target_link_libraries(
    hello_world
    PRIVATE cppzmq ${CMAKE_THREAD_LIBS_INIT}
)

# 一次性添加多个编译源和目标文件
# add_executable(可执行文件名  源文件)
# target_link_libraries(可执行文件名 PRIVATE cppzmq ${CMAKE_THREAD_LIBS_INIT} )

使用命令行选项编译代码。

如果 cppzmq 的头文件安装在 /usr/include/libzmq 库文件安装在 /usr/lib/
命令行的编译选项这么写

g++ --std=c++11 -pthread main.cpp -I/usr/include/ -o output -L /usr/lib/ -lzmq

zmq的通讯模式

以下表来自于:外部文章

一共有16种 socket 类型,使用时,socket类型和对端socket类型必须严格配对,否则无法正常工作;在有些socket类型中,bind和connect并无严格区分,如:xsub可以bind也可以connect;

通讯模式 socket类型 对端socket类型 方向 收发模式 发送策略 接收策略 静音状态
Client-Server ZMQ_CLIENT ZMQ_SERVER 双向 接收、发送 Round-robin Fair-queued Block
Client-Server ZMQ_SERVER ZMQ_CLIENT 双向 接收、发送 详见说明 Fair-queued EAGAIN
Radio-dish ZMQ_RADIO ZMQ_DISH 单向 发送 Fan out N/A Drop
Radio-dish ZMQ_DISH ZMQ_RADIO 单向 接收 N/A Fair-queued N/A
Pub-Sub ZMQ_PUB ZMQ_SUB、ZMQ_XSUB 单向 发送 Fan out N/A Drop
Pub-Sub ZMQ_SUB ZMQ_PUB、ZMQ_XPUB 单向 接收 N/A Fair-queued N/A
Pub-Sub ZMQ_XPUB ZMQ_SUB、ZMQ_XSUB 单向 发送:消息
接收:订阅主题
Fan out N/A Drop
Pub-Sub ZMQ_XSUB ZMQ_PUB、ZMQ_XPUB 单向 接收:消息
发送:订阅主题
N/A Fair-queued Drop
Pipeline ZMQ_PUSH ZMQ_PULL 单向 发送 Round-robin N/A Block
Pipeline ZMQ_PULL ZMQ_PUSH 单向 接收 N/A Fair-queued Block
Exclusive pair ZMQ_PAIR ZMQ_PAIR 双向 接收、发送 N/A N/A Block
Native pattern ZMQ_STREAM N/A 双向 接收、发送 详见说明 Fair-queued EAGAIN
Req-Rep ZMQ_REQ ZMQ_REP、ZMQ_ROUTER 双向 接收、发送 Round-robin Last peer Block
Req-Rep ZMQ_REP ZMQ_REQ、ZMQ_DEALER 双向 接收、发送 Last peer Fair-queued N/A
Req-Rep ZMQ_DEALER ZMQ_ROUTER、ZMQ_REP、ZMQ_DEALER 双向 接收、发送 Round-robin Fair-queued Block
Req-Rep ZMQ_ROUTER ZMQ_DEALER、ZMQ_REQ、ZMQ_ROUTER 双向 接收、发送 详见说明 Fair-queued Drop

示例六 网络聊天室

Client-Server 通讯模式,双向接收-发送。

Server 功能:

  • 处理和分发消息
  • 为降低复杂度,Server 收到来自 Client 的消息后,不发送确认消息
  • Client 发送给 Server 的消息,由 Server 播送给全体 Client

Client功能:

  • 连接 Server,向 Server 发送消息,从 Server 接收消息
  • Clinet1 用户A-连接 Server
  • Clinet2 用户B-连接 Server

Server.cpp 代码

#include <zmq.hpp>
#include <zmq_addon.hpp>
#include <iostream>
#include <thread>
#include <chrono>
#include <unordered_map>
#include <mutex>

// 存储客户端信息
std::unordered_map<std::string, std::string> client_names;
std::mutex clients_mutex;

void broadcast_message(zmq::socket_t& router, 
                      const std::string& sender_id,
                      const std::string& message) {
    std::lock_guard<std::mutex> lock(clients_mutex);
    
    for (const auto& client : client_names) {
        if (client.first != sender_id) {  // 不发送给自己
            std::vector<zmq::const_buffer> frames = {
                zmq::buffer(client.first),      // 目标客户端ID
                zmq::str_buffer(""),            // 空帧(DEALER要求)
                zmq::buffer(message)            // 消息内容
            };
            auto result = zmq::send_multipart(router, frames, zmq::send_flags::dontwait);
            if (!result) {
                std::cerr << "Failed to broadcast to client: " << client.first << std::endl;
            }
        }
    }
}

int main() {
    zmq::context_t ctx(1);
    zmq::socket_t router(ctx, zmq::socket_type::router);
    
    // 绑定到所有网络接口
    router.bind("tcp://0.0.0.0:6666");
    std::cout << "Chat Server started on tcp://192.168.1.146:6666" << std::endl;
    
    // 设置socket选项
    router.set(zmq::sockopt::rcvtimeo, 100);  // 100ms接收超时
    router.set(zmq::sockopt::sndtimeo, 100);  // 100ms发送超时
    
    while (true) {
        std::vector<zmq::message_t> msgs;
        zmq::recv_result_t result = zmq::recv_multipart(
            router, std::back_inserter(msgs), zmq::recv_flags::dontwait);


        if (result && *result >= 3) {
            std::string client_id = msgs[0].to_string();
            std::string empty_frame = msgs[1].to_string();  // DEALER的空帧
            std::string message = msgs[2].to_string();
            
            // 处理连接消息
            if (message.substr(0, 8) == "CONNECT:") {
                std::string username = message.substr(8);
                {
                    std::lock_guard<std::mutex> lock(clients_mutex);
                    client_names[client_id] = username;
                }
                std::cout << "Client connected: " << username 
                          << " (ID: " << client_id << ")" << std::endl;
                
                // 发送欢迎消息
                std::string welcome = "Server: Welcome " + username + "!";
                std::vector<zmq::const_buffer> frames = {
                    zmq::buffer(client_id),
                    zmq::str_buffer(""),
                    zmq::buffer(welcome)
                };
                auto result = zmq::send_multipart(router, frames, zmq::send_flags::dontwait);
                if (!result) {
                    std::cerr << "Failed to send welcome message" << std::endl;
                }
                // 广播新用户加入
                std::string broadcast_msg = "Server: " + username + " joined the chat";
                broadcast_message(router, client_id, broadcast_msg);
                
            } else if (message == "DISCONNECT") {
                std::string username;
                {
                    std::lock_guard<std::mutex> lock(clients_mutex);
                    if (client_names.count(client_id)) {
                        username = client_names[client_id];
                        client_names.erase(client_id);
                    }
                }
                if (!username.empty()) {
                    std::cout << "Client disconnected: " << username << std::endl;
                    std::string broadcast_msg = "Server: " + username + " left the chat";
                    broadcast_message(router, client_id, broadcast_msg);
                }
                
            } else {
                // 普通聊天消息
                std::string username;
                {
                    std::lock_guard<std::mutex> lock(clients_mutex);
                    if (client_names.count(client_id)) {
                        username = client_names[client_id];
                    } else {
                        username = "Unknown";
                    }
                }
                
                std::cout << "[" << username << "]: " << message << std::endl;
                
                // 广播消息给其他客户端
                std::string broadcast_msg = "[" + username + "]: " + message;
                broadcast_message(router, client_id, broadcast_msg);
            }
        }
        
        // 避免CPU占用过高
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    
    return 0;
}

Client.cpp 代码

#include <zmq.hpp>
#include <zmq_addon.hpp>
#include <iostream>
#include <thread>
#include <string>
#include <atomic>
#include <mutex>
#include <condition_variable>

std::atomic<bool> running(true);
std::mutex cout_mutex;

// 接收消息的线程
void receiver_thread(zmq::socket_t& dealer, const std::string& username) {
    while (running) {
        zmq::message_t msg;
        zmq::recv_result_t result = dealer.recv(msg, zmq::recv_flags::dontwait);
        
        if (result) {
            std::string message = msg.to_string();
            {
                std::lock_guard<std::mutex> lock(cout_mutex);
                std::cout << "\r" << message << std::endl;
                std::cout << "[" << username << "]: " << std::flush;
            }
        }
        
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    zmq::context_t ctx(1);
    zmq::socket_t dealer(ctx, zmq::socket_type::dealer);
    
    // 设置客户端ID(用于服务器识别)
    std::string client_id = "client_" + std::to_string(time(nullptr));
    dealer.set(zmq::sockopt::routing_id, client_id);
    
    // 连接到服务器
    dealer.connect("tcp://192.168.1.146:6666");
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    // 设置socket选项
    dealer.set(zmq::sockopt::rcvtimeo, 100);
    dealer.set(zmq::sockopt::sndtimeo, 100);
    
    // 输入用户名
    std::string username;
    {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "Enter your username: ";
        std::getline(std::cin, username);
    }
    
    // 发送连接消息
    std::string connect_msg = "CONNECT:" + username;
    dealer.send(zmq::str_buffer(""), zmq::send_flags::sndmore);
    dealer.send(zmq::buffer(connect_msg), zmq::send_flags::dontwait);
    
    std::cout << "Connected to chat server as: " << username << std::endl;
    std::cout << "Type your messages (type '/quit' to exit)" << std::endl;
    std::cout << "[" << username << "]: " << std::flush;
    
    // 启动接收线程
    std::thread receiver(receiver_thread, std::ref(dealer), username);
    
    // 主线程处理用户输入
    std::string input;
    while (running && std::getline(std::cin, input)) {
        if (input == "/quit" || input == "/exit") {
            // 发送断开连接消息
            dealer.send(zmq::str_buffer(""), zmq::send_flags::sndmore);
            dealer.send(zmq::buffer("DISCONNECT"), zmq::send_flags::dontwait);
            running = false;
            break;
        } else if (input == "/users") {
            // 查询在线用户(需要服务器支持)
            dealer.send(zmq::str_buffer(""), zmq::send_flags::sndmore);
            dealer.send(zmq::buffer("/users"), zmq::send_flags::dontwait);
        } else if (!input.empty()) {
            // 发送普通消息
            dealer.send(zmq::str_buffer(""), zmq::send_flags::sndmore);
            dealer.send(zmq::buffer(input), zmq::send_flags::dontwait);
            std::cout << "[" << username << "]: " << std::flush;
        }
    }
    
    // 清理
    running = false;
    if (receiver.joinable()) {
        receiver.join();
    }
    
    std::cout << "Disconnected from chat server" << std::endl;
    return 0;
}

运行结果:

# Server
Client connected: liujinyi (ID: client_1766025131)
[liujinyi]: nihao

# Client
Server: Welcome liujinyi!
[liujinyi]: nihao
posted @ 2025-12-16 14:53  北纬31是条纬线哦  阅读(22)  评论(0)    收藏  举报