在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都交付给一个工人。在这一部分中,我们将做一些完全不同的事情 —— 我们将向多个消费者传递一个消息。这种模式被称为“发布/订阅”。
为了说明这个模式,我们将构建一个简单的日志系统。它将由两个程序组成 —— 第一个将发出日志消息,第二个将接收并打印它们。
在我们的日志系统中,接收程序的每一个运行副本都将得到消息。这样我们就可以运行一个接收器并将日志引导到磁盘;与此同时,我们还可以运行另一个接收器,并在屏幕上看到日志。
本质上,发布的日志消息将会被广播到所有的接收者。
1、Exchanges
在本教程的前一部分中,我们发送并接收来自队列的消息。现在是时候在 Rabbitmq 中引入完整的消息传递模型了。
让我们快速回顾一下我们在之前的教程中所介绍的内容:
- producer,生产者是发送消息的用户应用程序。
- queue,队列是储存消息的缓冲区。
- consumer,使用者是接收消息的用户应用程序。
RabbitMQ 消息传递模型的核心思想是,生产者从不直接向队列发送任何消息。实际上,通常情况下,Producer 甚至不知道消息是否会被发送到哪些队列。
相反,生产者只能将消息发送到 Exchange。Exchange 是一件非常简单的事情。一方面,它接收来自生产者的消息,另一端则将它们推送到队列中。交换必须确切地知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?它应该被附加到许多队列吗?或者它应该被丢弃。规则是由 Exchange 类型定义的。
有一些可用的 Exchange 类型:direct、topic、headers 和 fanout。我们将关注最后一个 —— fanout。让我们创建一个这种类型的交换,并称之为 logs
:
channel.exchangeDeclare("logs", "fanout");
fanout 交换机非常简单。正如你可能从名称中猜到的那样,它只会将收到的所有消息广播到它所知道的所有队列中。这正是我们需要的记录器。
列出所有的 Exchanges
要列出服务器上的 Exchanges,你可以使用 rabbitmqctl 命令:
sudo rabbitmqctl list_exchanges
在这个列表中会有一些 amq.* 的 Exchanges 和 default(未命名)的 Exchange。这些都是默认创建的,但是现在不太可能需要使用它们。
没有名字的交换机
在本教程的前几部分中,我们对 Exchange 一无所知,但仍然能够将消息发送到队列。因为我们使用的是默认的 Exchange,我们通过空字符串(“”)来识别它。
回想一下我们之前发布的信息:
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数是 Exchange 的名称。空字符串表示默认或匿名 Exchange:Exchange 将使用 routingKey
把消息路由到指定的队列中。
现在,我们可以发布到我们指定的 Exchange:
2、临时 queues
你可能还记得,我们使用的队列有指定的名称(记得 hello
和 task_queue
吗?)能够命名队列对我们来说是至关重要的 —— 我们需要将 worker 指向同一个队列。当你想要在生产者和消费者之间共享队列时,给予队列名称是很重要的。
但这与我们的记录器无关。我们想要听到所有的日志消息,而不仅仅是它们的一个子集。我们也只对当前流动的信息感兴趣,而不是旧的消息。为了解决这个问题,我们需要两样东西。
首先,当我们连接到 Rabbitmq 时,我们需要一个空队列。为了做到这一点,我们可以创建一个带有随机名称的队列,或者更好——让服务器为我们选择一个随机队列名称。
其次,一旦我们断开了消费者的连接,队列应该被自动删除。
在 Java 客户端,当我们不向 queueDeclare()
提供参数时,我们会创建一个非持久的、排他的、自动删除队列,并生成一个名称:
String queueName = channel.queueDeclare().getQueue();
你可以在queues 指南中了解更多关于排他性标志和队列的其他属性信息.
在这个时候 queueName
包含一个随机队列名称。例如,它可能看起来像amq-jzty20brgko-hjjj0wlg
。
Bindings
我们已经创建了一个 fanout 类型的 Exchange 和一个队列。现在,我们需要告诉 Exchange 将消息发送到我们的队列。Exchange 和队列之间的关系称为绑定。
channel.queueBind(queueName, "logs", "");
从现在开始,日志 Exchange将向我们的队列追加消息。
绑定清单
你可以使用 rabbitmqctl
列出现有的绑定,
rabbitmqctl list_bindings
把它们的关系放在一起
3、发布与订阅
发布日志消息的生产者程序与之前的教程没有太大的区别。最重要的变化是,我们现在想要将消息发布到我们的日志 Exchange中,而不是匿名的。我们需要在发送时提供一个 routingKey
,但是在 fanout 类型的 Exchange 它的值被忽略了。这是 EmitLog.java 程序:
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = "info: Hello World!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
如你所见,在建立连接之后,我们声明了 Exchange。这一步是必要的,因为禁止发布消息到不存在的 Exchange
如果没有队列绑定到 Exchange,消息将会丢失,但这对我们来说是可以的;如果没有消费者在监听,我们可以放心地丢弃这个信息。
下面就是 ReceiveLogs.java 的代码。
ReceiveLogs.java
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
首先运行 ReceiveLogs.java
订阅日志消息,然后运行 EmitLog.java
发布一条消息:
我们再来看一下两个订阅者实例:
Subscribe - 1
Subscribe - 2
可以看到当生产者发送了一条日志消息到 Rabbitmq,然后订阅这个日志消息的两个实例都接收到这个消息并打印到控制台。
使用 rabbitmqctl list_bindings
,您可以验证代码实际上是否按照我们的需要创建绑定和队列。有两个 ReceiveLogs.java
java 程序在运行,应该看到类似下面的东西:
[root@localhost ~]# rabbitmqctl list_bindings
Listing bindings
logs exchange amq.gen-LQucjNPOJTtvLFb3d6-mcQ queue []
logs exchange amq.gen-y_-lYdEnSaGrA6Ze_t-yOA queue []
结果是很明显的:Exchange 日志中的数据使用服务器分配的名称到两个队列。这正是我们预期的。