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

浙公网安备 33010602011771号