3 Publish/Subscribe


官方文档地址: 3 Publish/Subscribe


同时向许多消费者发送消息。

前提条件

本教程假设你已经安装了 RabbitMQ 并在本地主机端口(5672)上运行。

发布 / 订阅

在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务只交付给一个worker。在这一部分中,我们将做一些完全不同的事情 – 我们将向多个消费者传递一个消息。这种模式称为“发布/订阅”。

为了演示该模式,我们将构建一个简单的日志系统。它将由两个程序组成 – 第一个程序将发出日志消息,第二个程序将接收和打印消息。

在我们的日志系统中,消费者程序的每个运行副本都将获得消息。这样我们就可以运行一个消费者将日志定向到磁盘;同时,我们运行另一个消费者将日志打印到控制台。

实际上,发布的日志消息将被广播给所有的接收者。

交换器

在之前的教程中,我们只是向队列发送和接收消息。现在是时候在 RabbitMQ 中引入完整的消息传递模型了。

让我们快速回顾一下之前教程中所涵盖的内容:

  • 生产者是发送消息的用户应用程序。
  • 队列是存储消息的缓冲区。
  • 消费者是接收消息的用户应用程序。

RabbitMQ 消息传递模型的核心思想是,生产者从不直接向队列发送任何消息。实际上,通常生产者甚至不知道消息是被传递到哪个队列了。

相反,生产者只能向exchange发送消息。交换是一件非常简单的事情。一方面接收来自生产者的消息,另一方面将消息推送到队列。exchange必须清楚地知道如何处理接收到的消息。它应该被发送到一个特定的队列吗?它应该被发送到许多队列吗?或者它应该被丢弃。这些规则由exchange类型定义。

有几种交换器类型:directtopicheadersfanout。这篇教程将使用最后一个 – 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());

临时队列

你可能还记得以前我们使用具有特定名称的队列(还记得hellotask_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交换器的数据进入两个具有服务器分配名称的队列。这正是我们想要的。

猜你喜欢

转载自blog.csdn.net/wb1046329430/article/details/115281608