【RabbitMQ】Publish/Subscribe
Publish/Subscribe
在上一节我们创建了一个work queue。背后的设想为每个任务被分发给明确的消费者。这节内容我们将做一些完全不同的事情 -- 我们将发送一条消息给多个消费者。这种模式被称为“发布/订阅”。
为了描述这种模式,我们来构建一个简单的日志系统。它包含两个程序 -- 第一个将会发送日志消息,第二个接收并打印。在我们的日志系统中,所有的正在运行的接收程序都会收到消息。这样我们可以运行一个接收程序,将日志定向到磁盘;同时可以运行另外的接收程序可以从屏幕上看到日志。
本质上,发布的所有日志消息会被广播给所有的接受者。
交换机
在前几节内容中,我们都是从一个队列中发送和接收消息。现在是时候介绍RabbitMQ的完整消息模型了。
快速回顾前面章节:
- 一个生产者是一个发送消息的用户应用
- 一个队列是一个存放消息的缓冲区
- 一个消费者是一个接收消息的用户应用
RabbitMQ消息模型的核心思想是,生产者从来不会直接发送消息给一个队列。确切的说,大多数情况下,生产者根本不知道它的消息将会发送到哪个队列。
事实是,生产者只能发送消息给一个交换机(exchange)。交换机是一个很简单的概念。一方面它接收生产者的消息,另一方面它推送消息到队列中。但是交换机必须明确知道自己要对接收到的消息进行何种处理:是添加到制定队列?还是添加到所有的队列?抑或是将之丢弃?这些规则由交换机的类型来定义。
有许多可供选择的交换机类型:direct, topic, headers, fanout. 我们集中在fanout上讲解。创建一个fanout类型的交换机,称它为logs:
channel.exchangeDeclare("logs", "fanout");
这个交换机非常简单。它会广播所有接收到的消息给所有它的已知队列。这就是我们logger程序所需要的。
Listing exchanges
可以使用rabbitmqctl来列出你服务器上的所有交换机:
$ sudo rabbitmqctl list_exchanges
Listing exchanges ...
direct
amq.direct direct
amq.fanout fanout
amq.headers headers
amq.match headers
amq.rabbitmq.log topic
amq.rabbitmq.trace topic
amq.topic topic
logs fanout
...done.
这个列表中有一些amp.*的交换机和默认(未命名)的交换机。它们都是默认被创建的。
Nameless exchange
之前的章节中我们对交换机一无所知,但却仍然可以发送消息到队列中。这很可能是因为我们使用了默认的交换机,我们用空字符串("")标识了它。
回想我们如何发布一条消息的:
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数就是交换机的名字。空字符串表示默认或未命名的交换机:消息根据路由Key指定的队列名称被路由到队列。
现在我们可以发布到我们自己命名的交换机:
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
你可能还记得我们之前使用的队列都是有一个指定的名称的(比如hello和task_queue)。给队列命名对我们来说至关重要 -- 我们需要将消费者指向相同的队列。当你想在生产者和消费者之间分享队列的时候,给队列一个名字非常重要。
但我们的日志程序不是这样的。我们希望监听所有的日志消息,而不仅仅是其中的一部分。我们也仅仅对当前的流动消息感兴趣,而不是老的消息。解决这个问题需要下面两件事:
第一,无论何时我们连接到RabbitMQ时,都需要一个新的空的队列。要做到这一点我们可以创建一个随机名称的队列,或者更好一点的方式 - 让服务器为我们选择一个随机队列名。
第二,一旦我们断开生产者的连接,队列应该被自动删除。
在Java客户端,当我们调用无参的queueDeclare()方法,我们将创建一个非持久化的,唯一的,自动删除的并且随机名称的队列。
String queueName = channel.queueDeclare().getQueue();
queueName是一个随机的队列名称,可能看起来像:amq.gen-JzTY20BRgKO-HjmUJj0wLg
绑定
现在我们已经创建了一个fanout交换机和一个队列。现在我们需要告诉交换机给我们的队列发送消息。这种在交换机和队列之间的关系叫做绑定(binding)。
channel.queueBind(queueName, "logs", "");
现在开始,logs交换机将向我们的队列追加消息。
Listing bingdings
使用rabbitmqctl list_bindings列出所有存在的绑定
Putting it all together
生产者程序,发送日志消息,看起来和之前的程序没有什么太大的区别。最重要的改变在我们现在希望发布消息到logs交换机,而不是之前的没有名字的交换机。在发送的时候,我们需要提供一个路由Key(routingKey),但是它的值被fanout交换机忽略了。下面是EmitLog.java:
import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Channel; public class EmitLog { private static final String EXCHANGE_NAME = "logs"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); String message = getMessage(argv); channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8")); System.out.println(" [x] Sent '" + message + "'"); channel.close(); connection.close(); } private static String getMessage(String[] strings){ if (strings.length < 1) return "info: Hello World!"; return joinStrings(strings, " "); } private static String joinStrings(String[] strings, String delimiter) { int length = strings.length; if (length == 0) return ""; StringBuilder words = new StringBuilder(strings[0]); for (int i = 1; i < length; i++) { words.append(delimiter).append(strings[i]); } return words.toString(); } }
如你所见,在创建了连接之后,我们声明了交换机。这一步是必须的,因为无法向一个不存在的交换机发布消息。
如果没有队列绑定到交换机上,消息会丢失,但对我们来说这没有什么,如果没有消费者监听我们可以安全的丢弃消息。
ReceiveLogs.java:
import com.rabbitmq.client.*; import java.io.IOException; public class ReceiveLogs { private static final String EXCHANGE_NAME = "logs"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(EXCHANGE_NAME, "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); } }