RabbitMQ(四) - 发布/订阅(Publish/Subscribe)

发布/订阅

在上一个教程中我们创建了一个工作队列。如果说工作队列是将一个任务完全分发给一个消费者。在这部分,我们所做的完全不同 —— 我们将把一个消息交付给多个消费者。这种模式称之为发布/订阅(publish/subscribe)。

为了说明这种模式,我们创建一个简单的日志系统。它由两个程序组成 —— 第一个(生产者)将发出一个日志消息,第二个(消费者)将接收它并打印它。

在我们的日志系统中每个接收者程序都能得到消息的拷贝。这样我们就可以跑一个接收者并直接将日志记录到磁盘;与此同时能跑另一个接收者将日志打印在屏幕。

本质上,发布一个日志消息将被广播到所有的接收者。

交换机(exchange)

在前面的教程中,我们向队列发送消息,从队列中接收消息。现在是时候介绍Rabbit中完整的消息传递模型了。

让我们快速的回顾下之前的教程:

  • 生产者是一个用来发送消息的用户应用
  • 队列用来缓存存储消息
  • 消费者是一个用来接收消息的用户应用

RabbitMQ的消息模型的核心思想是生产者不会直接的向队列发送任何消息。实际上,生产者都不知道将消息交付给哪个队列。

相反的,生产者只将消息往exchange中发送,exchange是非常简单的。一边从生产者中接收消息,一边将消息推到队列。exchange能正确的知道如何处理接收到的消息。是添加到一个特定的队列?是添加到很多队列?还是会丢弃。这些规则都通过exchange类型来定义。

RabbitMQ(四) - 发布/订阅(Publish/Subscribe)_第1张图片
rabbitmq-exchange

exchange有以下几种类型:

  • direct
  • topic
  • headers
  • fanout

我们目前关注最后一个 —— fanout。让我们新建一个这样的exchange类型,并命名为logs

channel.exchangeDeclare("logs", "fanout");

fanout exchange非常简单。你可以尽可能的从它的名字中猜测,它只是将接收到的消息广播到它所知道的队列。这正是我们需要的日志。

exchange 列表

你想在服务中查看exchange的列表,你能使用命令 rabbitmqctl

sudo rabbitmqctl list_exchanges

在列表中有些默认的、格式为amq.*的exchange。这些都是默认创建的,但是你不太可能需要使用到它们。

匿名 exchange

在前面的教程中我们对exchange一无所知,但是我们还是能将消息发送到队列。之所以可以,因为我们使用了默认的exchange,就是我们用的空字符串("")。

回想我们之前发布的一个消息:

channel.basicPublish("", "hello", null, message.getBytes());

第一个参数就是exchange的名称。空字符表示默认的或者匿名的exchange。消息路由到指定routingKey的队列中,如果它存在。

现在,我们能发布我们命名的exchange了:

channel.basicPublish( "logs", "", null, message.getBytes());

临时队列

还记得前面教程中我们使用的队列都是指定名字的(hello、work.queue)。命名一个队列对我们来是至关重要的——我们需要指定工作者到相同的队列中。当你想在生产者和消费者之间共享队列时,给队列命名是很重要的。

但是在我们的日志中者不是我们要关心的。我们想得到所有的日志消息,而不是他们的子集。我们感兴趣的也只是当前活动的消息而不是老的那个。为了解决这个我们需要做两个步骤:

第一,无论何时我们连接Rabbit需要一个新的,空的队列。要达到这个我们需要在创建队列的时候给它随机一个名字,或者更好方案 —— 让服务选择一个随机队列名给我们。

第二,一旦我们消费完队列断开连接就自动的删除队列。

我们提供了一个无参方法queueDeclare()创建一个非持久化、独有的、自动删除的、随机生成名字的队列。

String queueName = channel.queueDeclare().getQueue();

队列名是随机的,它的格式类似于:amq.gen-JzTY20BRgKO-HjmUJj0wLg

绑定(bindings)

rabbitmq-binding

我们已经创建了一个fanout exchange和一个队列。现在我们需要告诉exchange发送消息到我们的队列。这种exchange和队列的关联关系称之为绑定binding

channel.queueBind(queueName, "logs", "");

现在在logs exchange中可以将消息附加到我们的队列

绑定列表

你能显示当前正在使用的bindings列表

rabbitmqctl list_bindings

信息汇总

RabbitMQ(四) - 发布/订阅(Publish/Subscribe)_第2张图片
rabbitmq-three-overall

这个发送日志消息的生产者程序,和之前的教程没有相差很多。最重要的改变是我们用名称为logs的exchange代替了匿名的写法。当我们发送消息的时候要提供一个routingKey,但是在类型为fanout的exchange中,我们忽略它。下面是EmitLog.java整个程序的代码。

package com.roachfu.tutorial.rabbitmq.website.fanout;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class EmitLog {

    private static final 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();

        // 定义 exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        String message = "this is fanout exchange";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

如你所见,建立连接之后我们定义了exchange。这步是必要的,推送到一个不存在的exchange是不予许的。

如果没有队列绑定到exchange,消息将被丢失,但是对我们来说是可以的;如果没有消费者监听消息,我们能安全的丢弃消息。

官网使用命令行实现的。能很好的实现是显示在控制台还是记录到日志文件中。这里我们写两个程序,一个用来在console控制台打印消息,一个用来将消息记录到日志文件中。

ReceiveLogsToConsole.java

package com.roachfu.tutorial.rabbitmq.website.fanout;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 打印消息到控制台
 */
public class ReceiveLogToConsole {

    private static final 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();

        // 定义exchange,消费者和生产者都要定义。因为并不知道exchange存不存在。
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 获取队列名
        String queryName = channel.queueDeclare().getQueue();
        // 将队列名和exchange进行绑定
        channel.queueBind(queryName, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for message and handle it to console . . . ");

        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 '" + message + "'");
            }
        };

        channel.basicConsume(queryName, true, consumer);
    }
}

ReceiveLogToFile.java

package com.roachfu.tutorial.rabbitmq.website.fanout;

import com.rabbitmq.client.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 接收消息并打印到文件
 */
public class ReceiveLogToFile {

    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        Connection connection = connectionFactory.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 message and handle it to file. . . ");

        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");

                File file = new File("/Users/temp/fanout.log");

                FileOutputStream out = new FileOutputStream(file, true);
                out.write(body);
                out.write(("\r\n").getBytes());

                out.flush();
                out.close();
            }
        };

        channel.basicConsume(queueName, true, consumer);
    }
}

测试结果

我们先启动两个消费者,然后再启动三次生产者。

ReceiveLogToConsole.java 控制台显示:

 [*] Waiting for message and handle it to console . . . 
 [x] Received message 'this is fanout exchange'
 [x] Received message 'this is fanout exchange'
 [x] Received message 'this is fanout exchange'

ReceiveLogToFile.java 文件显示

RabbitMQ(四) - 发布/订阅(Publish/Subscribe)_第3张图片
rabbitmq-logtofile

你可能感兴趣的:(RabbitMQ(四) - 发布/订阅(Publish/Subscribe))