一、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 选项判断是否还有后续帧。

七、知识体系总览

下面这张图把本章的核心内容串起来了。你可以把它当作一个快速索引:

ZeroMQ 知识体系 上下文 (Context) Socket 类型 (6种) 请求-应答 REQ / REP 发布-订阅 PUB / SUB 管道 PUSH / PULL 消息发送 (zmq_send) / 消息接收 (zmq_recv) 多部分消息 (ZMQ_SNDMORE / ZMQ_RCVMORE)

从上下文出发,创建不同类型的 Socket,然后选择对应的消息模式,最后通过 zmq_send 和 zmq_recv 收发数据。多部分消息是进阶用法,但非常实用。

好了,这一章的内容就到这里。ZeroMQ 的核心概念其实不多,但每个都很关键。下一章我们会深入代码,看看实际项目中怎么用这些模式搭建通信系统。


公众号:蓝海资料掘金营,微信deep3321