文章目录
发布/订阅
Publish/Subscribe(using the java client)
在之前的章节中,我们创建了一个工作队列。工作队列背后的假设是每个任务只交付给一个工作者。在这一部分中,将会做一些完全不同的事情——向多个消费者传递一条消息。这种模式称为“发布/订阅”。
为了说明该模式,将构建一个简单的日志系统。它将由两个程序组成——第一个程序将发出日志消息,第二个程序将接收并打印它们。
在构建的日志系统中,每个运行的接收程序的副本都将获得消息。通过这种方式,能够运行一个接收者并将日志定向到磁盘。与此同时,可以运行另一个接收者,并在屏幕上看到日志。
实际上,发布的日志消息将被广播到所有的接收者。
Exchanges(交换机)
在之前的章节中,我们向队列发送和接收消息。现在是时候介绍rabbitMQ中完整的消息模型了。
回顾一下在之前的章节中介绍的内容:
- 生产者是一个发送消息的用户应用程序。
- 队列是存储消息的缓冲区。
- 消费者是接收消息的用户应用程序。
RabbitMQ消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,通常生产者甚至根本不知道消息是否会被发送到任何队列。
相反,生产者只能向交换机发送消息。交换机是很简单的东西。一方面它从生产者那里接收消息,另一方面它将消息推送到队列中。交换机必须确切地知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?是否应该将其添加到多个队列中?或者它应该被丢弃。这些规则由交换类型定义。
有一些可用的交换类型:direct
、topic
、header
和fanout
。现在先来关注最后一个,fanout
。首先创建一个这种类型的交换机,并将其称为logs
:
channel.exchangeDeclare("logs", "fanout");
fanout
交换机非常简单。它只是将它接收到的所有消息广播到它所知道的所有队列。这正是当前的日志系统所需要的。
交换机列表
要列出服务器上的交换机,可以使用非常有用的
rabbitmqctl
命令:
rabbitmqctl list_exchanges
在这个列表中会有一些
amq.*
的交换机和默认的(未命名的)交换机。这些都是默认情况下创建的,但是目前不太可能需要使用它们。
未命名的交换机
在前面的章节中,我们对交换机一无所知,但仍然能够将消息发送到队列。这是可能的,因为使用的是默认交换机,由空字符串(
""
)标识。
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数是交换机的名称。空字符串表示默认的或未命名的交换机:如果
routingKey
存在,消息将被路由到由其指定的名称的队列。
现在,可以将其发布到已命名的交换机:
channel.basicPublish("logs", "", null, message.getBytes());
临时队列
之前使用的队列都具有特定的名称(例如
hello
和task_queue
)。能够命名一个队列对我们来说非常重要——需要将工作者指向同一个队列。当希望在生产者和消费者之间共享队列时,为队列指定一个名称是很重要的。
但日志系统不是这样的。我们希望了解所有日志消息,而不仅仅是其中的一个子集。并且还只对当前流动的消息感兴趣而不是旧消息。要解决这个问题,需要做两件事。
首先,当连接到rabbitMQ时,需要一个新的并且为空的队列。要做到这一点,可以创建一个随机名称的队列,或者更好的是——让服务器选择一个随机的队列名称。
其次,一旦和消费者断开连接,队列就会被自动删除。
在java客户端中,当不向queueDeclare()
传参时,就会创建一个非持久的、独占的、自动删除的队列,并使用一个生成的名称。
String queueName = channel.queueDeclare().getQueue();
此时,
queueName
包含一个随机队列名。例如amq.gen-JzTY20BRgKO-HjmUJj0wLg
(可以在guide on queues中了解有关exclusive
标志和其他的队列属性的更多信息)。
绑定
我们已经创建了
fanout
交换机以及队列。现在,需要告诉交换机向队列发送消息。交换机和队列之间的这种关系称为绑定。
channel.queueBind(queueName, "logs", "");
从现在开始,
logs
交换机将向队列添加消息。
绑定列表
可以使用以下方法列出现有的绑定:
rabbitmqctl list_bindings
整合代码
生产者程序发出日志消息,与前面章节中的没有太大的不同。最重要的变化是,我们现在希望将消息发布到
logs
交换机,而不是未命名的。需要在发送时提供一个routingKey
,但是它的值对于fanout
交换机会被忽略。下面是EmitLog.java
程序的代码:
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
var factory = new ConnectionFactory();
factory.setHost("192.168.1.254");
factory.setUsername("admin");
factory.setPassword("admin123");
try (var connection = factory.newConnection();
var channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" : String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
正如所看到的那样,在建立连接之后声明了交换机。这一步是必要的,因为发布到一个不存在的交换机是被禁止的。
如果还没有队列绑定到交换机上,消息就会丢失,但这对我们来说是可以的。此时如果还没有消费者在监听,可以安全地丢弃该消息。
ReceiveLogs.java
的代码:
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
var factory = new ConnectionFactory();
factory.setHost("192.168.1.254");
factory.setUsername("admin");
factory.setPassword("admin123");
var connection = factory.newConnection();
var 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");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}