1. 一句话认识 ZeroMQ(libzmq)
ZeroMQ(简称 ZMQ) 是一个开源的、跨平台的高性能消息库 ,核心实现 libzmq 由 C/C++ 编写,采用 LGPLv3+(带静态链接例外) 许可,官方站点 zeromq.org,代码仓库 github.com/zeromq/libzmq。
它不是消息队列中间件(不是 RabbitMQ 那种 broker),而是被作者称为 "轻量消息内核(messaging kernel)" 或 "套接字加强版(sockets on steroids)" 的东西:它不运行任何独立进程,直接以库的形式链接进你的应用程序,通过一组"模式化套接字"(patterned sockets)提供消息通信能力。
🧠 通俗类比
- 传统消息队列(如 RabbitMQ):所有消息都要先送到"中央邮局(broker)",由邮局分拣后再投递给收件人。
- ZeroMQ :没有中央邮局,寄件人和收件人直接对接。它给你的是"自带分拣规则的智能信箱"------你按既定模式(请求-应答、发布-订阅、推-拉等)约定好收发规则,剩下的重连、排队、路由、分帧它全包了。
1.1 核心数据指标(官方 perf 套件)
| 场景 | 数据 |
|---|---|
| 1 字节消息平均往返延迟(TCP) | 约 30.9 µs |
| 小消息吞吐(早期官方测试) | 约 2.6M msg/s(单流),8 路机累计约 4M msg/s |
| 512 字节消息 | 可跑满 10GbE 网卡 |
| inproc 传输 | 约 40M msg/s,延迟 <1µs(社区基准) |
| ipc 传输 | 约 10M msg/s,延迟约 10µs(社区基准) |
| tcp(LAN) | 约 2M msg/s,延迟约 100µs(社区基准) |
这些数字说明一件事:ZeroMQ 的性能在"内嵌库"这个级别几乎无出其右,这正是它被量化交易、实时行情、HPC 场景偏爱的原因。
2. 核心架构与设计哲学
2.1 四大设计原则
| 原则 | 含义 |
|---|---|
| No broker(无代理) | 没有中央节点,通信双方通过 API 直连,"网络即代理(The network is the broker)" |
| Smart endpoints, dumb network | 复杂度放在应用端(端点),网络本身保持简单------和"智能网络、傻瓜终端"的传统思路正好相反 |
| Patterns, not protocols | 你选择的是"消息模式"(请求-应答/发布-订阅...),而不是某个具体协议;模式之上再套序列化格式 |
| Zero-copy where possible | 尽量零拷贝:inproc 共享内存、大消息可用 zmq_msg_init_data 零拷贝发送 |
2.2 去中心化无代理(Brokerless)意味着什么
- 没有独立的 broker 进程 / 守护进程需要部署、监控、扩容;
- 消息不流经中央节点,物理上消除了至少一次网络跳数(RTT)与中间件处理延迟;
- 任何节点都可以随时加入或退出,剩余节点继续工作------没有单点故障(SPOF);
- 代价是:路由、可靠性、服务发现等原本 broker 承担的逻辑,需要应用自己负责(后面坑点章节会展开)。
2.3 零拷贝与异步 I/O 引擎
libzmq 的底层做得非常"抠":
- 所有 I/O 在后台线程完成 :应用线程调用 zmq_send() / zmq_recv() 不会阻塞在系统调用上,消息先进入用户态队列,由 I/O 线程批量发送。
- Opportunistic batching(机会主义批处理):一次 writev() 尽量发出多条消息,摊薄系统调用开销------这是它吞吐高于"每消息一次 send"的关键。
- inproc:// 传输走共享内存 ,同一进程内线程间通信零拷贝;
- TCP_NODELAY 默认开启,禁用 Nagle 算法,避免小消息被延迟合并;
- 内部使用无锁(lock-free)数据结构,对象之间通过"消息传递命令(commands)"通信,每个对象独占一个线程,无锁、无线程阻塞。
2.4 全局状态与并发模型
- libzmq 没有全局变量;所有全局状态封装在 context(上下文)对象中(C++ 内部类 ctx_t),例如 inproc 端点表、等待 linger 的 socket 列表;
- 你可以在不同线程从同一个 context 创建多个 socket,context 本身是线程安全的;
- socket 不是线程安全的------一个 socket 严禁被多个线程同时使用(这是最常见的崩溃来源,详见第 11 节)。
3. 五种核心消息模式:ZMQ 的灵魂
ZeroMQ 的 socket 按消息模式(pattern)分类,而不是按原始字节流。每个模式封装了"连接管理、消息分帧、自动重连、排队、路由"等一套经验规则。除 ZMQ_PAIR 外,socket 都可以多对多连接 ,且 zmq_bind 与 zmq_connect 没有严格的"服务端/客户端"语义,调用顺序可以任意(先 connect 再 bind 也合法)。
| 模式 | Socket 类型 | 行为 | 典型场景 |
|---|---|---|---|
| 请求-应答 | REQ / REP | REQ 严格"发送→接收"交替;请求在多个 REP 间 round-robin;REP 公平排队(fair-queue)接收请求,并把回复路由回最近请求方 | 同步 RPC、任务分发、微服务调用 |
| 发布-订阅 | PUB / SUB(扩展 XPUB/XSUB) | PUB 单向 fan-out 广播;SUB 默认不订阅任何消息,必须设 ZMQ_SUBSCRIBE 前缀过滤 | 行情推送、事件广播、日志分发、传感器数据 |
| 管道 | PUSH / PULL | PUSH 向多个 PULL 轮询(round-robin)发送;PULL 公平排队接收;单向、可靠(节点不掉落则不丢消息) | 并行任务分发、工作队列、多阶段流水线 |
| 异步路由 | DEALER / ROUTER | DEALER 是 REQ 的异步版(无交替约束,round-robin 发送 + fair-queue 接收);ROUTER 接收时自动在消息前加 identity 帧,发送时移除该帧以路由 | 异步客户端、多路服务端、负载均衡、自定义 broker |
| 独占对 | PAIR | 仅能与一个对端连接,无路由/过滤;无自动重连,不适合 TCP(仅推荐 inproc 进程内线程通信) | 单进程内两线程专线通信 |
3.1 模式对应的 RFC
| 模式 | RFC |
|---|---|
| 请求-应答 | RFC 28 |
| 发布-订阅 | RFC 29 |
| 管道(PIPELINE) | RFC 30 |
| 独占对(EXPAIR) | RFC 31 |
3.2 合法连接组合速查
PUB-SUB、REQ-REP、REQ-ROUTER(REQ 自动插入空帧)、DEALER-REP(REP 期望空帧)、DEALER-ROUTER、DEALER-DEALER、ROUTER-ROUTER、PUSH-PULL、PAIR-PAIR。
3.3 多部分消息(Multipart)与 ROUTER 信封
ZeroMQ 允许把多个 frame 拼成一条逻辑消息(用 ZMQ_SNDMORE 标志标记"后面还有")。最常见的是 ROUTER 的信封结构:
[identity 帧][空分隔符帧][payload 帧...]
- ROUTER 接收时自动在消息头部附加对端的 identity 帧;
- 回复时必须把该 identity 帧原样带回,否则消息静默丢失;
- 忘记空分隔符帧是 ROUTER 场景最常见的 bug;
- DEALER 连接 REP 时,每条消息必须含一个空帧作为分隔符,后跟一个或多个正文帧。
4. 为什么值得用:七大使用优点
4.1 低延迟 / 高吞吐(性能天花板)
前面 1.1 的数据已经说明:µs 级延迟、百万级 msg/s 吞吐。相比"每条消息一次系统调用"的朴素实现,它的用户态队列 + 批量写 + 无锁结构是性能的根本来源。同语言生态里,Java 绑定(JeroMQ)小消息也只比 C++ 慢几微秒,大消息(>512B)吞吐与 C++ 持平------说明性能优势在库本身,而非语言。
4.2 40+ 语言绑定
核心是 C/C++(libzmq),生态覆盖 Python(PyZMQ)、Java(JeroMQ)、.NET(NetMQ)、Go、Rust、Ruby、Node.js 等 40+ 语言。同一套消息模式语义,跨语言互通------C++ 发布者可以把消息发给 Python 订阅者。
4.3 跨平台
Linux / Windows / macOS / Android,支持 10+ 操作系统 × 多种架构(ARM 到 Itanium)。嵌入式、桌面、服务器通吃。
4.4 多传输抽象,切换零成本
| 传输 | 前缀 | 特点 |
|---|---|---|
| TCP | tcp:// | 跨机器,最常用 |
| IPC | ipc:// | 本机进程间(Unix 域套接字 / Windows named pipe) |
| inproc | inproc:// | 同进程内线程间,共享内存零拷贝,需同一 context 且先 bind 再 connect |
| 多播 | pgm:// / epgm:// | 可靠多播(Pragmatic General Multicast) |
| UDP | udp:// | 无连接传输(v4 起) |
业务代码只需要改一个 URL 字符串,通信传输方式即可切换,其余代码零改动。
4.5 内置背压处理(HWM 高水位)
每个连接管道都有独立的高水位(High Water Mark, HWM),默认 1000 条(ZMQ_SNDHWM / ZMQ_RCVHWM 可调):
- PUB / ROUTER 达到 HWM:静默丢弃新消息;
- DEALER / PUSH / PULL 等其他类型:阻塞(TCP 自然背压),或 ZMQ_DONTWAIT 时返回 EAGAIN。
这让"慢消费者"有了明确可控的行为边界(虽然丢与不丢需要你按场景选型,见第 11 节)。
4.6 弹性:任意顺序启动、动态加入退出、自动重连
组件可以任何顺序启动(先启动消费者再启动生产者也行),断线后自动重连,消息恰好落在断线前一刻------这是"无代理 + 智能端点"带来的天然弹性,极大简化了分布式组件的部署顺序约束。
4.7 安全:CurveZMQ(v4+)
v4 引入 CurveZMQ :基于 Curve25519 椭圆曲线 + AES-128-CBC 加密,提供 256-bit 等效强度(≈3072-bit RSA),使用临时会话密钥实现前向保密;另有 NULL / PLAIN 机制与 ZAP 认证协议。详见第 9 节。
5. 适用场景:从行情分发到并行流水线
| 场景 | 推荐模式 | 为什么适合 ZeroMQ |
|---|---|---|
| 金融行情分发 | PUB/SUB | 极低延迟 + 高吞吐,容忍少量丢包,不容忍延迟------正是 PUB 的语义 |
| 并行任务分发 / 工作队列 | PUSH/PULL | 天然 round-robin 分发,多阶段流水线 |
| 微服务同步调用 | REQ/REP | 轻量 RPC,无需引入重量级框架 |
| 异步多路服务端 | DEALER/ROUTER | 自定义负载均衡、路由、服务目录 |
| 进程内多线程通信 | PAIR / PUSH-PULL(inproc) | 零拷贝共享内存,替代手写线程队列 |
| IoT / 边缘网关 | PUB/SUB + proxy | 低资源占用、可内嵌、多传输切换 |
| 日志 / 监控数据汇聚 | PUSH/PULL | 多源汇聚、背压清晰 |
| HPC / 仿真协同 | PUSH/PULL、PUB/SUB | 高吞吐低延迟,配合 ipc/tcp 切换 |
⚠️ 边界提示:ZeroMQ 不做持久化 (消息只在内存队列里),不保证可靠投递(best-effort)。需要"落盘 + 至少一次/精确一次投递"的场景(订单、审计、事件溯源)请转向 RabbitMQ / Kafka(见第 10 节)。
6. 环境准备:安装与编译
6.1 包管理器快速安装
bash
# Ubuntu / Debian
sudo apt install libzmq3-dev
# macOS
brew install zeromq
# Windows(vcpkg,推荐)
vcpkg install cppzmq # 同时安装 libzmq
6.2 源码编译(CMake)
bash
# 1. 编译安装 libzmq(核心库)
git clone https://github.com/zeromq/libzmq.git
cd libzmq && mkdir build && cd build
cmake .. # 需要 CURVE 加密时加 -DWITH_LIBSODIUM=ON
make -j4
sudo make install
# 2. 编译安装 cppzmq(C++ 头文件封装)
git clone https://github.com/zeromq/cppzmq.git
cd cppzmq && mkdir build && cd build
cmake -DCPPZMQ_BUILD_TESTS=OFF ..
sudo make install
6.3 CMake 集成(推荐现代方式)
find_package(cppzmq REQUIRED)
target_link_libraries(your_target PRIVATE cppzmq)
6.4 命令行编译(原生 C API)
bash
g++ app.cpp $(pkg-config --cflags --libs libzmq) -o app
7. 具体使用方式一:原生 C API 核心步骤
libzmq 的 C API 是"五步走":创建 context → 创建 socket → bind/connect → send/recv → 关闭清理 。下面用 REQ/REP 同步请求-应答 写一个完整可运行示例,覆盖全部关键 API。
7.1 完整可运行示例:REQ/REP 回显服务器
cpp
// reqrep.c ------ ZeroMQ 原生 C API 最小示例
// 编译: gcc reqrep.c $(pkg-config --cflags --libs libzmq) -o reqrep
#include <zmq.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
int main(void) {
/* Step 1: 创建上下文(全局状态容器,默认 1 个 I/O 线程)
* 经验法则:每 1Gbps 吞吐加 1 个 I/O 线程,且不超过 CPU 核数-1 */
void *ctx = zmq_ctx_new();
assert(ctx);
/* Step 2: 创建 socket ------ 指定消息模式 */
void *responder = zmq_socket(ctx, ZMQ_REP); // 应答方
void *requester = zmq_socket(ctx, ZMQ_REQ); // 请求方
/* Step 3: bind / connect(顺序可以任意,本示例用 inproc 避免端口冲突) */
int rc = zmq_bind(responder, "inproc://echo"); // 绑定端点
assert(rc == 0);
rc = zmq_connect(requester, "inproc://echo"); // 连接端点
assert(rc == 0);
/* Step 4: 发送 / 接收
* 注意 REQ 严格"先 send 后 recv",REP 严格"先 recv 后 send",不可乱序 */
char buf[256];
snprintf(buf, sizeof(buf), "Hello, ZeroMQ!");
rc = zmq_send(requester, buf, strlen(buf), 0); // 发送请求
assert(rc == (int)strlen(buf));
rc = zmq_recv(responder, buf, sizeof(buf), 0); // REP 接收请求
assert(rc > 0);
printf("[REP] received: %s\n", buf);
rc = zmq_send(responder, buf, rc, 0); // REP 回复(原样返回)
assert(rc > 0);
rc = zmq_recv(requester, buf, sizeof(buf), 0); // REQ 接收回复
assert(rc > 0);
printf("[REQ] received: %s\n", buf);
/* Step 5: 关闭 socket 并终止上下文 */
zmq_close(responder);
zmq_close(requester);
zmq_ctx_term(ctx); // 清理 I/O 线程与资源
return 0;
}
关键 API 一览:
| API | 作用 |
|---|---|
| zmq_ctx_new() / zmq_ctx_set(ctx, ZMQ_IO_THREADS, N) | 创建 / 配置上下文 |
| zmq_socket(ctx, ZMQ_REP) | 按模式创建 socket |
| zmq_bind(sock, "tcp://*:5555") | 绑定端点(* 表示所有网卡) |
| zmq_connect(sock, "tcp://localhost:5555") | 连接端点 |
| zmq_send(sock, buf, len, flags) / zmq_recv(...) | 发送 / 接收(简单字节 API) |
| zmq_msg_init_size() + zmq_msg_send() / zmq_msg_recv() | 结构化消息 API(避免 zmq_recv 截断大消息) |
| zmq_poll(zmq_pollitem_t\[\], n, timeout) | 多 socket 轮询 |
| zmq_proxy(frontend, backend, capture) | 转发 / 队列 / 捕获设备 |
| zmq_setsockopt() / zmq_getsockopt() | 配置选项(必须在 bind/connect 前设置) |
| zmq_close(sock) / zmq_term(ctx) | 关闭清理 |
7.2 多部分消息与轮询(PUSH/PULL 工作队列)
cpp
// multipart_poll.c ------ 多部分消息 + zmq_poll 轮询要点
#include <zmq.h>
#include <stdio.h>
#include <string.h>
int main(void) {
void *ctx = zmq_ctx_new();
void *push = zmq_socket(ctx, ZMQ_PUSH);
void *pull = zmq_socket(ctx, ZMQ_PULL);
zmq_bind(push, "tcp://127.0.0.1:5560");
zmq_connect(pull, "tcp://127.0.0.1:5560");
/* 多部分消息:第一部分带 ZMQ_SNDMORE 标志,表示后面还有帧 */
const char *part1 = "header", *part2 = "payload";
zmq_send(push, part1, strlen(part1), ZMQ_SNDMORE);
zmq_send(push, part2, strlen(part2), 0);
/* zmq_poll:同时监听多个 socket(zmq_pollitem_t 数组) */
zmq_pollitem_t items[] = {
{ pull, 0, ZMQ_POLLIN, 0 }, // socket, fd, events, revents
};
int rc = zmq_poll(items, 1, 1000); // 超时 1000ms,返回就绪数
if (rc > 0 && (items[0].revents & ZMQ_POLLIN)) {
char buf[256];
int n = zmq_recv(pull, buf, sizeof(buf), 0);
printf("received part: %.*s\n", n, buf);
}
zmq_close(push);
zmq_close(pull);
zmq_ctx_term(ctx);
return 0;
}
8. 具体使用方式二:cppzmq 现代 C++ 封装
cppzmq 是 libzmq 官方的 C++ 头文件封装(zmq.hpp + zmq_addon.hpp,C++11+,仅头文件,RAII 管理资源),是你日常写 C++ 的首选。
8.1 关键 RAII 类型
| 类型 | 说明 |
|---|---|
| zmq::context_t | 上下文,析构时自动清理 |
| zmq::socket_t | 套接字,析构自动 close |
| zmq::message_t | 消息对象 |
| zmq::socket_ref | 非拥有型 socket 引用 |
| zmq::poller_t | C++ 轮询器(替代 zmq_poll) |
| zmq::monitor_t | 连接状态监控 |
8.2 完整可运行示例:PUSH/PULL 多部分消息(TCP 动态端口)
cpp
// pushpull.cpp ------ cppzmq 完整示例
// 编译: g++ -std=c++17 pushpull.cpp -lzmq -o pushpull
#include <zmq.hpp>
#include <zmq_addon.hpp>
#include <array>
#include <vector>
#include <string>
#include <iostream>
int main() {
// 1. 创建上下文与 socket(RAII,无需手动释放)
zmq::context_t ctx;
zmq::socket_t push(ctx, zmq::socket_type::push); // 推送端
zmq::socket_t pull(ctx, zmq::socket_type::pull); // 拉取端
// 2. 绑定 TCP 动态端口,通过 last_endpoint 拿到实际端口
push.bind("tcp://127.0.0.1:*");
std::string ep = push.get(zmq::sockopt::last_endpoint);
std::cout << "listening on " << ep << std::endl;
pull.connect(ep); // 连接动态端口
// 3. 发送多部分消息(两个 frame 组成一条逻辑消息)
std::array<zmq::const_buffer, 2> msg = {
zmq::str_buffer("foo"),
zmq::str_buffer("bar!")
};
zmq::send_multipart(push, msg); // zmq_addon.hpp 提供
// 4. 接收多部分消息
std::vector<zmq::message_t> recvd;
zmq::recv_multipart(pull, std::back_inserter(recvd));
for (const auto& m : recvd)
std::cout << "frame: " << m.to_string() << std::endl;
// 5. RAII 析构自动关闭 socket / 终止上下文
return 0;
}
8.3 常用 cppzmq 用法速查
cpp
// 订阅(SUB 必须先设置订阅过滤,否则收不到任何消息)
zmq::socket_t sub(ctx, zmq::socket_type::sub);
sub.set(zmq::sockopt::subscribe, ""); // 订阅全部
sub.set(zmq::sockopt::subscribe, "A"); // 前缀过滤:只收以 "A" 开头的消息
sub.connect("tcp://localhost:5556");
// 非阻塞接收
zmq::message_t msg;
if (sub.recv(msg, zmq::recv_flags::dontwait)) {
// 有数据,msg 已填充
}
// 轮询(zmq::poller_t,替代 C 的 zmq_poll)
zmq::poller_t poller;
poller.add(sub, zmq::event_flags::pollin);
auto events = poller.wait(std::chrono::milliseconds(100));
if (events.size() > 0) { /* sub 有数据可读 */ }
9. 具体使用方式三:高级特性(Proxy 与 CURVE 加密)
9.1 Proxy 代理设备:zmq_proxy 单调用转发
zmq_proxy(frontend, backend, capture) 是 libzmq 最被低估的 API 之一:一个调用 即可实现消息转发、队列缓冲、流量捕获。典型拓扑是 XPUB/XSUB(扩展发布/订阅 socket):
cpp
// proxy.cpp ------ XPUB/XSUB 转发代理
// 编译: g++ -std=c++17 proxy.cpp -lzmq -o proxy
#include <zmq.hpp>
#include <thread>
#include <chrono>
#include <iostream>
int main() {
zmq::context_t ctx;
// 前端:面向订阅者的 XPUB(收集订阅请求)
zmq::socket_t frontend(ctx, zmq::socket_type::xpub);
frontend.set(zmq::sockopt::xpub_verbose, 1); // 关键:设为 1 才能收到订阅/退订事件
frontend.bind("tcp://*:5559");
// 后端:面向发布者的 XSUB(接收发布消息)
zmq::socket_t backend(ctx, zmq::socket_type::xsub);
backend.bind("tcp://*:5560");
// 运行代理:单调用完成"订阅转发 + 消息分发 + 队列"
zmq::proxy(frontend, backend); // 可选第三参数 capture socket
return 0;
}
典型用途:
- 桥接 PUB/SUB 两端(如外部 TCP 地址 ↔ 内部任务之间);
- 流量捕获与监控(第三参数 capture socket 旁路复制所有消息);
- 构建服务目录:利用 ZMQ_XPUB_VERBOSE 收到订阅/取消订阅事件,动态记录谁订阅了什么。
9.2 CURVE 加密:v4+ 安全通信(需 libsodium)
CurveZMQ 基于 Curve25519(公钥加密)+ AES-128-CBC,密钥是 Z85 编码的 40 字符字符串。角色与 bind/connect 方向无关;一个 socket 只能有一种安全级别(不能同一 PUB 混用 PLAIN 与 CURVE 订阅者)。
第一步:生成密钥对
bash
# 命令行工具(libzmq 自带)
curve_keygen
# 输出示例:
# CURVE PUBLIC KEY : Yne@$w-vo<fVvi]a<NY6T1ed:M$fCG*[IaLV{hID
# CURVE SECRET KEY : D:)Q[IlAW!ahhC2ac:9*A}h:p?e4~WMz3j6P3M23
或 C API:zmq_curve_keypair(public_key, secret_key);cppzmq:zmq::curve_keypair()(返回 std::pair<公钥, 私钥>)。
第二步:服务端 socket 设置(只设 SECRET,不设 PUBLIC)
cpp
// server.c ------ CURVE 服务端
#include <zmq.h>
int main(void) {
void *ctx = zmq_ctx_new();
void *server = zmq_socket(ctx, ZMQ_REP);
const char *secret = "D:)Q[IlAW!ahhC2ac:9*A}h:p?e4~WMz3j6P3M23";
int is_server = 1;
zmq_setsockopt(server, ZMQ_CURVE_SERVER, &is_server, sizeof(is_server));
zmq_setsockopt(server, ZMQ_CURVE_SECRETKEY, secret, 40); // 仅服务端设 SECRET
zmq_bind(server, "tcp://*:9000");
/* ... 正常 recv/send ... */
return 0;
}
第三步:客户端 socket 设置(需要服务端公钥 + 自己的密钥对)
cpp
// client.c ------ CURVE 客户端
#include <zmq.h>
int main(void) {
void *ctx = zmq_ctx_new();
void *client = zmq_socket(ctx, ZMQ_REQ);
const char *server_pub = "Yne@$w-vo<fVvi]a<NY6T1ed:M$fCG*[IaLV{hID"; // 服务端公钥
const char *client_pub = "<自己的公钥>";
const char *client_sec = "<自己的私钥>";
zmq_setsockopt(client, ZMQ_CURVE_SERVERKEY, server_pub, 40); // 服务端公钥
zmq_setsockopt(client, ZMQ_CURVE_PUBLICKEY, client_pub, 40); // 自己的公钥
zmq_setsockopt(client, ZMQ_CURVE_SECRETKEY, client_sec, 40); // 自己的私钥
zmq_connect(client, "tcp://localhost:9000");
/* ... 正常 send/recv ... */
return 0;
}
进阶:可叠加 ZAP(ZeroMQ Authentication Protocol),在 inproc:// 端点上运行一个 REP 认证处理器,实现公钥白名单级别的接入控制。
10. 横向对比:ZeroMQ vs RabbitMQ vs Kafka vs NNG vs Asio
| 维度 | ZeroMQ | RabbitMQ | Kafka | nanomsg / NNG | Boost.Asio |
|---|---|---|---|---|---|
| 架构 | Brokerless 库(P2P) | Broker(AMQP) | 分布式分区日志 | Brokerless 库 | 网络 I/O 框架(非 MQ) |
| 部署 | 无需服务,代码集成 | 需安装/集群 | 需集群(ZK/KRaft) | 无需服务 | 库,自行实现协议 |
| 持久化 | 无内置(内存队列) | 磁盘持久化、ACK、重传 | 磁盘、复制、分区 | 无内置 | 无 |
| 吞吐量 | 十万~百万级 msg/s(内嵌库百万级) | 万级 | 十万~百万级 | 0.5M~1M msg/s | 取决于实现(接近原生 socket) |
| 延迟 | µs 级(最低,inproc <1µs) | µs~ms 级 | ms 级 | 5--30µs | µs 级(裸 TCP/异步) |
| 可靠性 | best-effort,可能丢 | 高(至少一次) | 极高(exactly/at-least) | best-effort | 自行保证 |
| C++ 支持 | libzmq C/C++,cppzmq | librabbitmq (C) | librdkafka C/C++ | C 核心,C++ 兼容 | 纯 C++ 头库 |
| 协议/模式 | 多模式 REQ/REP PUB/SUB PUSH/PULL DEALER/ROUTER | AMQP/MQTT/STOMP,exchange 路由 | 自定义二进制,pub/sub + 流 | SP 协议(含 SURVEY) | 裸 TCP/UDP/串口 |
| 安全 | CURVE/ZAP(v4+) | TLS/插件 | TLS/SASL | NNG 原生 TLS 1.2/1.3 | TLS/SSL 自行集成 |
| 许可 | LGPLv3+(静态链接例外) | MPL 2.0 | Apache 2.0 | MIT | Boost(宽松) |
| 最佳场景 | 低延迟内嵌管道、HFT、IPC、实时数据 | 企业异步解耦、复杂路由 | 大数据流、日志、事件源 | 轻量 IPC、嵌入式、现代 C++ | 自定义协议/异步网络 |
选型结论:
- 要复杂路由 / 可靠投递 / 持久化 → RabbitMQ / Kafka;
- 要极致低延迟、去中心、轻量内嵌管道、C++ 高性能通信 → ZeroMQ;
- NNG 许可更宽松、库更小、原生 TLS,是"更现代轻量"的同类替代;
- Asio 是底层 I/O 框架,消息语义(模式、重连、路由)需要自己实现。
互操作性提示:NNG 实现了相同的 Scalability Protocols,REQ/REP、PUB/SUB、PIPELINE 线格式与 ZeroMQ 兼容 ;但 DEALER/ROUTER 是 ZeroMQ 专有扩展,两者不互通。
11. 常见坑点与最佳实践
11.1 消息大小上限与流控
- 消息本质是内存 blob,理论上 0 到 GB 级都行;但超大消息会拖慢 ROUTER 等分帧 socket;
- HWM 按条数计,不按字节计 ------大数据流务必切 chunk 并自行流控,切勿设无限 HWM(内存直接 OOM)。
11.2 HWM 高水位:丢还是阻塞,取决于模式
| Socket 类型 | 达 HWM 行为 |
|---|---|
| PUB / ROUTER | 静默丢弃新消息 |
| DEALER / PUSH / PULL | 阻塞(或 ZMQ_DONTWAIT 返回 EAGAIN) |
- 默认 1000(v3+);v2.x 默认无限,高量发布者必须显式设置;
- inproc 两端共享缓冲,实际 HWM 为双方之和;
- HWM 并非精确值,实际缓冲可能低至标称值的一半;
- 所有 socket 选项必须在 zmq_bind/zmq_connect 之前设置,否则可能对后续连接不生效。
11.3 慢消费者:丢消息是特性不是 bug
PUB 端的慢订阅者不会让发布者感知------消息直接丢弃。这要求你按场景选型:
- 行情、传感器等"容忍丢、不容忍延迟"场景:PUB/SUB 正合适;
- 任务、订单等"绝不能丢"场景:用 PUSH(需大 HWM + 容忍阻塞)或异步发送队列,监控队列深度用 ZMQ_EVENTS + ZMQ_POLLOUT。
11.4 Linger 选项:不设置可能卡死进程
- ZMQ_LINGER 默认 -1(无限等待) ,socket close 时若队列里还有未发出消息会一直阻塞,进程可能退出不了;
- 0 表示立即丢弃;
- 建议设合理值(如 1000ms),确保关键消息在 close 前发出;
- 注意:必须先关闭 context 内所有 socket,才能 zmq_term。
11.5 线程安全:socket 非线程安全
- 严禁跨线程共享同一个 socket(随机崩溃);
- context 线程安全:可在不同线程从同一 context 创建多个 socket;
- 线程间通信用 inproc://(必须同一 context,且先 bind 再 connect,inproc 不是断连传输)。
11.6 REQ/REP 顺序约束与"破损态"
REQ 严格"send→recv"交替。超时 recv 会让 REQ 进入破损态 ,之后所有操作异常,必须销毁重建。工程上使用 Lazy Pirate 模式:超时 + 重试,N 次失败放弃并换新 socket。
11.7 订阅时序:SUB 先订阅、PUB 晚点发
- SUB 必须先 ZMQ_SUBSCRIBE,否则收不到任何消息;
- PUB 先于 SUB 启动会丢失早期消息(PUB 不会排队给尚未连接的订阅者)------建议 SUB 先启动,或切换 bind/connect 方向让 SUB 做 bind。
11.8 PUSH/PULL 的"不公平"分发
首个连上的 PULL 会抢走不成比例的消息,待所有 PULL 连上后才真正轮询(相差仅数毫秒)。低速率或需要严格负载均衡时,改用 ROUTER/DEALER 组合。
11.9 ROUTER 信封:忘记 identity 帧 = 静默丢消息
- 接收为 identity空分隔符payload,回复必须包含 identity 帧;
- 建议设 ZMQ_ROUTER_MANDATORY,并检查每次 send 的返回值;
- 忘记空分隔符帧是最常见 bug。
11.10 Context 与序列化
- 每应用一个 context(多 context 浪费资源且破坏 inproc);
- 退出务必 zmq_term();仅做 inproc 通信时可设 I/O 线程数为 0;
- ZeroMQ 只传裸字节,复杂数据用 JSON / Protobuf / MessagePack 自行序列化;
- ZeroMQ 不保证投递,需要可靠投递时在其上实现 Lazy Pirate(重试)、Paranoid Pirate(心跳)、Majordomo(服务化 broker)等可靠模式。
12. 总结
ZeroMQ(libzmq)用一组"模式化套接字"把消息通信的复杂度收敛到了库内部:无代理架构 带来极低延迟与无单点故障,五种消息模式 覆盖请求-应答、发布-订阅、任务管道、异步路由与线程专线,多传输抽象 让 TCP/IPC/inproc 切换零成本,cppzmq 让现代 C++ 使用体验接近"STL 级"顺手。
回到系列定位:Asio 解决"字节怎么高效流动",Beast 解决"字节怎么按 HTTP/WebSocket 解读",gRPC 解决"跨语言服务怎么按接口调用",而 ZeroMQ 解决"消息怎么按模式在任意端点之间直达"------它补上了从"传输"到"消息语义"的最后一层,而且这层不需要任何中间件进程。
一句话决策 :如果你的系统需要的是"低延迟、去中心、可内嵌的进程/线程/机器间消息管道",ZeroMQ 就是那个值得放进工具箱的"轻量消息内核"。唯一要记住的是:它把可靠性留给了应用------别指望它替你保证"不丢",要自己在其上构建可靠的模式。
FAQ 速查表
| 问题 | 一句话回答 |
|---|---|
| ZeroMQ 是消息队列中间件吗? | 不是,它是无代理(brokerless)的消息库,直接链接进应用,无独立进程 |
| REQ/REP 和 DEALER/ROUTER 什么区别? | REQ 严格交替收发、坏后需重建;DEALER 异步无交替约束,ROUTER 用 identity 帧路由 |
| SUB 收不到消息最常见原因? | 没设 ZMQ_SUBSCRIBE 订阅过滤;或 PUB 先启动导致早期消息丢失 |
| 怎么保证消息不丢? | ZeroMQ 本身 best-effort,需自建 Lazy Pirate / Paranoid Pirate / Majordomo 可靠模式 |
| HWM 是什么?默认多少? | 高水位(队列条数上限),默认 1000(v3+);PUB/ROUTER 超限丢,其余阻塞 |
| 大消息能发吗? | 能,但 HWM 按条计数、超大消息拖慢分帧 socket,建议切 chunk 自管流控 |
| 多线程能共享一个 socket 吗? | 不能,socket 非线程安全;线程间通信用 inproc + 各自 socket |
| 进程退出卡住怎么办? | 设置 ZMQ_LINGER(如 1000ms),并确 |