ZeroMQ(ZMQ)是一个高性能异步消息库,适用于构建可伸缩的分布式或并发应用。发布/订阅(PUB/SUB)模式是ZMQ支持的消息传递模式之一,在该模式下,发布者(Publisher)发送消息,订阅者(Subscriber)接收消息。订阅者可以订阅特定的消息主题,只有与其订阅匹配的消息会被接收。
发布/订阅模式的特性
- 一对多:一个发布者可以有多个订阅者,所有订阅者都会收到发布者发送的消息。
- 过滤消息:订阅者可以订阅特定的主题,只接收与主题匹配的消息。
- 异步通信:发布者和订阅者不需要同步操作,发布者可以继续发送消息,而不必等待订阅者接收。
- 松耦合:发布者和订阅者彼此独立,彼此不知道对方的存在,这使得系统更具可扩展性和灵活性。
使用场景
- 实时数据分发:股票行情、天气预报等需要将实时数据分发给多个接收者。
- 日志收集:应用程序可以将日志发送到集中式日志服务器,多个日志分析工具可以订阅这些日志。
- 通知系统:发布者发送通知消息,多个订阅者接收并处理这些通知,如消息推送、事件驱动的系统等。
C语言实现
下面是一个简单的C语言示例,演示了如何使用ZMQ的发布/订阅模式。这个示例包括一个发布者和一个订阅者。
安装ZeroMQ
在开始编写代码之前,请确保已安装ZeroMQ库。可以使用以下命令进行安装:
sudo apt-get install libzmq3-dev
发布者代码(publisher.c)
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建发布者套接字
void *publisher = zmq_socket(context, ZMQ_PUB);
zmq_bind(publisher, "tcp://*:5555");
while (1) {
// 构建消息
char message[20];
sprintf(message, "Hello %d", rand() % 100);
// 发送消息
zmq_send(publisher, message, strlen(message), 0);
printf("Sent: %s\n", message);
sleep(1);
}
// 清理
zmq_close(publisher);
zmq_ctx_destroy(context);
return 0;
}
订阅者代码(subscriber.c)
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建订阅者套接字
void *subscriber = zmq_socket(context, ZMQ_SUB);
zmq_connect(subscriber, "tcp://localhost:5555");
// 订阅所有消息(空字符串表示订阅所有主题)
zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, "", 0);
while (1) {
char buffer[20];
// 接收消息
zmq_recv(subscriber, buffer, 20, 0);
buffer[19] = '\0'; // 确保字符串以'\0'结尾
printf("Received: %s\n", buffer);
}
// 清理
zmq_close(subscriber);
zmq_ctx_destroy(context);
return 0;
}
编译和运行
使用以下命令编译和运行发布者和订阅者代码:
gcc -o publisher publisher.c -lzmq
gcc -o subscriber subscriber.c -lzmq
./publisher
./subscriber
首先运行publisher
,然后在另一个终端中运行subscriber
。你会看到发布者发送的消息被订阅者接收并打印出来。
以上示例展示了ZMQ发布/订阅模式的基本用法。通过这种模式,可以实现高效的消息分发和接收,适用于各种实时数据分发和事件通知的应用场景。
订阅指定Topic信息
在ZeroMQ的发布/订阅模式中,过滤消息是通过在订阅者设置订阅主题来实现的。发布者发送的消息可以包含主题信息,订阅者通过设置过滤规则来决定接收哪些消息。订阅者只会接收与其订阅主题匹配的消息。
如何实现消息过滤
1. 发布者代码
发布者需要在发送消息时包含主题信息。主题和消息之间用空格分隔。以下是修改后的发布者代码示例:
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建发布者套接字
void *publisher = zmq_socket(context, ZMQ_PUB);
zmq_bind(publisher, "tcp://*:5555");
while (1) {
// 构建消息
char message[30];
char *topic = "news";
sprintf(message, "%s %d", topic, rand() % 100);
// 发送消息
zmq_send(publisher, message, strlen(message), 0);
printf("Sent: %s\n", message);
sleep(1);
}
// 清理
zmq_close(publisher);
zmq_ctx_destroy(context);
return 0;
}
在这个示例中,每条消息都以一个主题开头,后面跟着消息内容。主题和消息之间用空格分隔。
2. 订阅者代码
订阅者通过设置订阅主题来过滤消息。订阅者可以选择只接收特定主题的消息。以下是修改后的订阅者代码示例,其中订阅者选择只接收以"news"为主题的消息:
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建订阅者套接字
void *subscriber = zmq_socket(context, ZMQ_SUB);
zmq_connect(subscriber, "tcp://localhost:5555");
// 订阅特定的主题(例如“news”)
char *subscription_topic = "news";
zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, subscription_topic, strlen(subscription_topic));
while (1) {
char buffer[30];
// 接收消息
zmq_recv(subscriber, buffer, 30, 0);
buffer[29] = '\0'; // 确保字符串以'\0'结尾
printf("Received: %s\n", buffer);
}
// 清理
zmq_close(subscriber);
zmq_ctx_destroy(context);
return 0;
}
在这个示例中,订阅者使用zmq_setsockopt
函数设置订阅主题。只接收以"news"为主题的消息。
运行多个订阅者
你可以运行多个订阅者,每个订阅者可以设置不同的主题来接收不同类型的消息。例如,你可以创建一个订阅者来接收"news"主题的消息,另一个订阅者来接收其他主题的消息。可以通过在zmq_setsockopt
中设置不同的主题来实现。
示例
-
运行发布者:
./publisher
-
运行第一个订阅者(接收"news"主题的消息):
./subscriber
-
如果需要测试其他主题,修改发布者代码中的主题内容,并重新编译,然后运行另一个订阅者实例进行测试。
这样,你就能实现消息的过滤,订阅者只会接收符合其订阅主题的消息。
Topic存在问题
在ZeroMQ的发布/订阅模式中,订阅者的主题匹配是基于消息的前缀进行的。具体来说,订阅者只会接收以其订阅主题为前缀的消息。这意味着如果一个订阅者订阅了news
主题,它将接收到所有以news
开头的消息,包括news2
这样的消息,因为news2
在news
的前缀范围内。
解释
当你订阅了news
主题时,ZeroMQ 实际上将这个订阅主题作为前缀匹配的模式处理。因此:
- 如果发布者发送的消息是
news 123
,这个消息将匹配news
主题的订阅者。 - 如果发布者发送的消息是
news2 456
,这个消息同样也会被news
主题的订阅者接收到,因为news2
以news
为前缀。
解决方案
如果你希望订阅者只接收确切匹配的主题,你可以使用更精确的匹配模式。ZeroMQ并不直接支持完全匹配的主题,但有几个方法可以达到类似的效果:
-
精确匹配
你可以在消息的内容中添加分隔符或特定格式,使得订阅者能够区分不同的消息。例如,使用特殊的字符作为分隔符,确保每个消息的主题都是唯一且清晰的。
-
实现更复杂的过滤
你可以在订阅者端进行更复杂的消息过滤。即使订阅者接收到所有匹配的消息,它也可以在接收消息后进一步解析和过滤,只处理符合特定条件的消息。
修改后的示例代码
发布者代码(publisher.c)
为了使主题更加明确,你可以在消息中使用更具体的格式:
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建发布者套接字
void *publisher = zmq_socket(context, ZMQ_PUB);
zmq_bind(publisher, "tcp://*:5555");
while (1) {
// 发布"news"主题的消息
char message[30];
snprintf(message, sizeof(message), "news: %d", rand() % 100);
zmq_send(publisher, message, strlen(message), 0);
printf("Sent: %s\n", message);
// 发布"news2"主题的消息
snprintf(message, sizeof(message), "news2: %d", rand() % 100);
zmq_send(publisher, message, strlen(message), 0);
printf("Sent: %s\n", message);
sleep(1); // 暂停1秒
}
// 清理
zmq_close(publisher);
zmq_ctx_destroy(context);
return 0;
}
订阅者代码(subscriber.c)
你可以在订阅者端进一步处理消息,确保只处理确切匹配的主题:
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建新的ZMQ上下文
void *context = zmq_ctx_new();
// 创建订阅者套接字
void *subscriber = zmq_socket(context, ZMQ_SUB);
zmq_connect(subscriber, "tcp://localhost:5555");
// 订阅"news"主题
char *subscription_topic = "news:";
zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, subscription_topic, strlen(subscription_topic));
while (1) {
char buffer[30];
// 接收消息
zmq_recv(subscriber, buffer, sizeof(buffer) - 1, 0);
buffer[sizeof(buffer) - 1] = '\0'; // 确保字符串以'\0'结尾
// 处理消息(仅处理以"news:"开头的消息)
if (strncmp(buffer, "news:", 5) == 0) {
printf("Received: %s\n", buffer);
} else {
printf("Ignored: %s\n", buffer);
}
}
// 清理
zmq_close(subscriber);
zmq_ctx_destroy(context);
return 0;
}
运行和测试
-
编译代码:
gcc -o publisher publisher.c -lzmq gcc -o subscriber subscriber.c -lzmq
-
运行发布者:
./publisher
-
运行订阅者(例如订阅
news:
主题):./subscriber
通过这些修改,你可以确保每个订阅者只处理特定主题的消息,并避免不需要的消息被接收。