官方文档地址: 3 Publish/Subscribe
同时向许多消费者发送消息。
前提条件
本教程假设你已经安装了 RabbitMQ 并在本地主机端口(5672)上运行。
发布 / 订阅
在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务只交付给一个worker
。在这一部分中,我们将做一些完全不同的事情 – 我们将向多个消费者传递一个消息。这种模式称为“发布/订阅”。
为了演示该模式,我们将构建一个简单的日志系统。它将由两个程序组成 – 第一个程序将发出日志消息,第二个程序将接收和打印消息。
在我们的日志系统中,消费者程序的每个运行副本都将获得消息。这样我们就可以运行一个消费者将日志定向到磁盘;同时,我们运行另一个消费者将日志打印到控制台。
实际上,发布的日志消息将被广播给所有的接收者。
交换器
在之前的教程中,我们只是向队列发送和接收消息。现在是时候在 RabbitMQ 中引入完整的消息传递模型了。
让我们快速回顾一下之前教程中所涵盖的内容:
- 生产者是发送消息的用户应用程序。
- 队列是存储消息的缓冲区。
- 消费者是接收消息的用户应用程序。
RabbitMQ 消息传递模型的核心思想是,生产者从不直接向队列发送任何消息。实际上,通常生产者甚至不知道消息是被传递到哪个队列了。
相反,生产者只能向exchange
发送消息。交换是一件非常简单的事情。一方面接收来自生产者的消息,另一方面将消息推送到队列。exchange
必须清楚地知道如何处理接收到的消息。它应该被发送到一个特定的队列吗?它应该被发送到许多队列吗?或者它应该被丢弃。这些规则由exchange
类型定义。
有几种交换器类型:direct
、topic
、headers
、fanout
。这篇教程将使用最后一个 – fanout
。让我们创建一个这种类型的exchange
,并命名为logs
:
channel.exchangeDeclare("logs", "fanout");
fanout
交换器非常简单。它只是将接收到的所有消息广播给它知道的所有队列。这正是我们的记录器所需要的。
交换器列表
要列出服务器上的交换器列表,你可以运行rabbitmqctl
:
sudo rabbitmqctl list_exchanges
在 Windows 上:
rabbitmqctl.bat list_exchanges
在这个列表中会有一些
amq.*
交换器和默认交换器(未命名)。这些是默认创建的,但你现在不太可能需要使用它们。
默认交换器
在之前的教程中,我们对exchange
一无所知,但仍然能够将消息发送到队列。这是因为我们使用了默认交换器,我们通过空字符串(""
)来标识它。回想一下我们之前发布的消息的代码:
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数是
exchange
的名称。空字符串表示默认交换器:消息被路由到由routingKey
指定的名称的队列,如果它存在的话。
现在,我们可以发布消息到我们命名的交换器,注意此时队列名称参数为空:
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
你可能还记得以前我们使用具有特定名称的队列(还记得hello
和task_queue
吗?)。能够命名一个队列对我们来说是至关重要的 – 当我们需要将worker
指向同一个队列,或者希望在生产者和消费者之间共享队列时,为队列指定名称非常重要。
但对于我们的记录器来说,情况并非如此。我们希望了解所有日志消息,而不仅仅是其中的一个子集。我们也只对当前流动的消息感兴趣,而对旧的消息不感兴趣。要解决这个问题,我们需要做两件事。
首先,每当我们连接到 RabbitMQ 时,我们需要一个新的空队列。为此,我们可以创建一个具有随机名称的队列,或者,更好的方法是让服务器为我们选择一个随机队列名称。
其次,一旦我们断开消费者的连接,队列应该被自动删除。
在 Java 客户端中,当我们不向queueDeclare()
提供任何参数时,我们将创建一个具有随机生成名称的非持久的、独占的、自动删除的队列。
String queueName = channel.queueDeclare().getQueue();
您可以在队列指南中了解有关排他标志exclusive
和其他队列属性的更多信息。
此时queueName
是一个一个随机队列名。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg
。
绑定
我们已经创建了一个fanout
交换器和一个队列。现在我们需要告诉交换器将消息发送到我们的队列。交换器和队列之间的关系称为绑定binding
。
channel.queueBind(queueName, "logs", "");
从现在开始,logs
交换器将向我们的队列追加消息。
绑定列表
可以列出现有的绑定:
rabbitmqctl list_bindings
在 Windows 上:
rabbitmqctl.bat list_bindings
把它们放一起
生产者程序发出日志消息,与前面的教程没有太大的不同。最重要的变化是,我们现在希望将消息发布到logs
交换器,而不是默认交换器。我们需要在发送时提供一个routingKey
,但fanout
交换器会忽略它(所以也就不提供了)。
EmitLog.java
类的代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author wangbo
* @date 2019/10/23 11:24
*/
public class EmitLog {
//交换器名称
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接器连接到服务器
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try(Connection connection = factory.newConnection()){
//创建一个通道
Channel channel = connection.createChannel();
//声明交换器,设置交换器类型 fanout
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
//从命令行接受参数
String message = args.length < 1 ? "info: Hello World!" : String.join(" ", args);
//发布消息到交换器
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
如您所见,在建立连接之后,我们声明了exchange
。这一步是必要的,因为发布到不存在的交换器是被禁止的。
如果没有队列绑定到exchange
,消息将丢失,但这对我们来说没有问题;如果还没有消费者在监听,我们可以放心地丢弃这些信息。
ReceiveLogs.java
类的代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author wangbo
* @date 2019/10/22 18:25
*/
public class ReceiveLogs {
//交换器名称
private final static String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接器连接到服务器
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明交换器,设置交换器类型 fanout
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(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
//消费者监听
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}
编译,为了方便,我们将对类路径使用环境变量$CP
(Windows 上是%CP%
):
javac -cp $CP EmitLog.java ReceiveLogs.java
如果你想将日志保存到文件中,只需打开控制台运行消费者:
java -cp $CP ReceiveLogs > logs_from_rabbit.log
如果你想在你的屏幕上看到日志,打开一个新的终端并运行消费者:
java -cp $CP ReceiveLogs
运行产生日志的生产者:
java -cp $CP EmitLog
使用rabbitmqctl list_bindings
,您可以验证代码是否实际创建了我们想要的绑定和队列。在运行两个ReceiveLogs.java
程序时,您应该会看到以下内容:
sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
# => logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
# => ...done.
对结果的解释很简单:来自logs
交换器的数据进入两个具有服务器分配名称的队列。这正是我们想要的。