一、ZeroMQ 到底是什么?
说实话,我第一次接触 ZeroMQ 的时候,心里想的是:「又一个消息队列?」
但用了一段时间后,我发现它跟 RabbitMQ、Kafka 那些东西完全不是一回事。ZeroMQ 不是一个独立的消息代理服务器,它本质上是一个网络通信库。你把它链接到你的程序里,它就能让你的程序跟其他程序「对话」。
我个人的理解是:ZeroMQ 把 Socket 编程中那些烦人的细节——连接管理、重连、消息分帧、缓冲——全都封装好了。你只需要关心「发什么」和「收什么」,不用管「怎么连」。
核心特点:
- 无代理架构:没有中央服务器,节点之间直接通信
- 极简 API:核心就几个函数,zmq_send、zmq_recv、zmq_bind、zmq_connect
- 自动重连:断线后自动尝试重连,你不需要写重连逻辑
- 高性能:底层用 C 写的,消息吞吐量非常可观
- 多语言绑定:C、C++、Python、Java、Go 等几十种语言都能用
嗯,这里要注意:ZeroMQ 不保证消息的持久化。如果程序崩溃了,内存里的消息就丢了。它追求的是「快」,不是「稳」。我在项目中用它做实时数据管道,从来没指望它帮我存数据。
二、三种消息模式
ZeroMQ 定义了三种基本的消息模式。我刚开始学的时候觉得有点绕,后来发现其实就是三种常见的通信场景。
1. 请求-应答模式(Request-Reply)
这个模式说白了就是「你问我答」。客户端发一个请求,服务端收到后回一个应答。一对一,严格配对。
我曾经在一个监控系统里用过这个模式。每个采集节点向中心服务器发送状态数据,服务器处理完后回复一个确认。代码写起来特别清爽:
// 服务端(REP)
void *context = zmq_ctx_new();
void *responder = zmq_socket(context, ZMQ_REP);
zmq_bind(responder, "tcp://*:5555");
while (1) {
char buffer[256];
zmq_recv(responder, buffer, 256, 0);
printf("收到请求: %s\n", buffer);
zmq_send(responder, "OK", 2, 0);
}
// 客户端(REQ)
void *requester = zmq_socket(context, ZMQ_REQ);
zmq_connect(requester, "tcp://localhost:5555");
zmq_send(requester, "Hello", 5, 0);
zmq_recv(requester, buffer, 256, 0);
注意:REQ 和 REP 必须严格交替使用。你发一个请求,就必须等一个应答,不能连续发两个请求。我见过有人在这个坑里卡了一整天。
2. 发布-订阅模式(Pub-Sub)
这个模式大家应该很熟悉。发布者把消息扔出去,订阅者只收自己感兴趣的消息。发布者不知道谁在收,订阅者也不知道谁在发。
我记得有一次做行情数据推送,一个发布者每秒发几万条价格数据,几十个订阅者各取所需。用 PUB-SUB 模式,代码量比用传统 Socket 少了至少一半。
// 发布者(PUB)
void *publisher = zmq_socket(context, ZMQ_PUB);
zmq_bind(publisher, "tcp://*:5556");
zmq_send(publisher, "AAPL 150.25", 12, 0);
// 订阅者(SUB)
void *subscriber = zmq_socket(context, ZMQ_SUB);
zmq_connect(subscriber, "tcp://localhost:5556");
zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, "AAPL", 4); // 只收AAPL
zmq_recv(subscriber, buffer, 256, 0);
避坑指南:订阅者必须设置订阅前缀,否则什么都收不到。我曾经忘了调用 zmq_setsockopt 设置 ZMQ_SUBSCRIBE,结果调试了半天才发现消息全被过滤掉了。
3. 管道模式(Pipeline)
管道模式也叫「扇出/扇入」模式。一个任务分发器把工作分给多个工人,工人处理完后把结果汇总到一个收集器。典型的「分而治之」思路。
我在一个图像处理项目里用过这个模式。主节点把图片分片发给 8 个工人节点并行处理,最后收集结果。速度提升了将近 7 倍。
// 分发器(PUSH)
void *ventilator = zmq_socket(context, ZMQ_PUSH);
zmq_bind(ventilator, "tcp://*:5557");
zmq_send(ventilator, task_data, task_len, 0);
// 工人(PULL + PUSH)
void *receiver = zmq_socket(context, ZMQ_PULL);
zmq_connect(receiver, "tcp://localhost:5557");
void *sender = zmq_socket(context, ZMQ_PUSH);
zmq_connect(sender, "tcp://localhost:5558");
// 收集器(PULL)
void *collector = zmq_socket(context, ZMQ_PULL);
zmq_bind(collector, "tcp://*:5558");
三、Socket 类型速查
ZeroMQ 的 Socket 类型其实就六种,对应三种模式。我整理了一张表,方便你对照:
| Socket 类型 | 所属模式 | 方向 | 典型用途 |
|---|---|---|---|
| ZMQ_REQ | 请求-应答 | 发送请求 | 客户端 |
| ZMQ_REP | 请求-应答 | 接收请求并回复 | 服务端 |
| ZMQ_PUB | 发布-订阅 | 只发送 | 发布者 |
| ZMQ_SUB | 发布-订阅 | 只接收 | 订阅者 |
| ZMQ_PUSH | 管道 | 发送任务 | 分发器/工人 |
| ZMQ_PULL | 管道 | 接收任务 | 工人/收集器 |
你想想看,其实就三种通信模式,每种两个角色。记住这个规律,后面学起来就轻松了。
四、上下文与 Socket
在 ZeroMQ 里,上下文(Context) 是一个全局的容器,管理着所有的 Socket 和 IO 线程。每个进程只需要创建一个上下文。
我个人习惯在程序一开始就创建上下文,然后在退出前销毁它:
void *context = zmq_ctx_new();
// ... 创建各种 Socket,做各种操作 ...
zmq_ctx_destroy(context);
Socket 则是具体的通信端点。一个上下文里可以创建多个 Socket,它们共享 IO 线程。我记得有一次我创建了 100 多个 Socket 做压力测试,上下文管理得井井有条,一个都没泄漏。
关键点:
- 一个进程一个上下文,不要每个线程都创建
- Socket 必须从上下文创建,不能独立存在
- 销毁顺序:先销毁所有 Socket,再销毁上下文
五、消息发送与接收
ZeroMQ 的消息发送和接收接口非常简洁。核心就两个函数:
int zmq_send(void *socket, void *buf, size_t len, int flags);
int zmq_recv(void *socket, void *buf, size_t len, int flags);
flags 参数常用的有:
ZMQ_DONTWAIT:非阻塞模式,立即返回0:阻塞模式,直到发送/接收完成
嗯,这里有个细节:zmq_send 返回的是发送的字节数,zmq_recv 返回的是接收的字节数。如果返回 -1,说明出错了。
我曾经犯过一个低级错误:在非阻塞模式下没检查返回值,结果数据没发出去也不知道。后来加了个日志,才发现问题。
六、多部分消息
多部分消息是 ZeroMQ 的一个特色功能。一条消息可以拆成多个「帧」(frame),每一帧独立发送和接收,但接收端会把这些帧组合成一条完整的消息。
举个例子,你想发送一个包含「消息类型」和「消息体」的消息:
// 发送多部分消息
zmq_send(socket, "CMD", 3, ZMQ_SNDMORE); // 还有更多
zmq_send(socket, "START", 5, 0); // 最后一部分
// 接收多部分消息
int more;
size_t more_size = sizeof(more);
do {
zmq_recv(socket, buffer, 256, 0);
zmq_getsockopt(socket, ZMQ_RCVMORE, &more, &more_size);
printf("收到帧: %s\n", buffer);
} while (more);
实用技巧:多部分消息非常适合做协议封装。比如第一帧放消息类型,第二帧放数据长度,第三帧放实际数据。我在做自定义协议时经常这么用,解析起来特别清晰。
注意 ZMQ_SNDMORE 标志,它告诉 ZeroMQ:「后面还有帧,别急着把消息送出去」。接收端通过 ZMQ_RCVMORE 选项判断是否还有后续帧。
七、知识体系总览
下面这张图把本章的核心内容串起来了。你可以把它当作一个快速索引:
从上下文出发,创建不同类型的 Socket,然后选择对应的消息模式,最后通过 zmq_send 和 zmq_recv 收发数据。多部分消息是进阶用法,但非常实用。
好了,这一章的内容就到这里。ZeroMQ 的核心概念其实不多,但每个都很关键。下一章我们会深入代码,看看实际项目中怎么用这些模式搭建通信系统。
公众号:蓝海资料掘金营,微信deep3321