RabbitMQ 教程- 发布/订阅

260 阅读7分钟

RabbitMQ 教程 - 发布订阅

(使用 amqp.node 客户端)

在上一个教程中我们创建了一个工作队列。工作队列背后的设定其实是每一个任务都会被传送到精确的一个工人。在这一节中我们将会做一些完全不一样的东西——我们将会给多个消费者传递一个消息。这种模式也被称为“发布/订阅”。

为了说明这种模式,我们准备搭建一个简单的日志系统。它将由两部分的程序组成——一个会发布日志消息并且另一个会接收并打印日志消息。

在我们的日志系统中,接收程序的每一个运行副本都将会获得消息。通过这种方式,我们将可以使用一个接收程序来直接把日志写入磁盘;并且同时我们可以跑着另一个接收程序并把打印结果输出到屏幕上供我们查看。

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

exchanges(交换器)

在先前的章节中,我们只是在一个队列中直接收发消息。现在,是时候介绍rabbitmq中全部的消息模型了。

让我们快速的回顾一下,在之前的章节中我们讲述了什么:

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

RabbitMQ消息模型核心的思想就是生产者从来不直接给一个队列发送任何消息。实际上,通常生产者甚至不知道一条消息是否会被发送到任何队列中。

取而代之,生产者仅可以给一个交换机发送消息。一个交换机是非常简单的东西。它从生产者一端接收消息并且从另一端将消息再推送到各个队列中。交换机必须明确的知道该如何处理它接收到的每条消息。这条消息应该被添加到一个特定的队列?这条消息应该被添加到很多队列?或者这条消息应该被丢弃。这些规则都是可以通过交换器的类型*(exchange type)*来定义的。

img

这里有一些交换机的类型是可供选择的:direct,topicheadersfanout。我们将会注重讲解最后一个——fanout。让我们常见一个这种类型的交换器,并且取名为logs

ch.assertExchange('logs', 'fanout', {durable: false})

fanout交换器非常的简单。你可以从它的名字中很容易的猜到它的涵义,它会广播自己收到的所有信息给它知道的所有队列。并且者也是我们的日志系统明确需要的。

查看交换器

如果你想要在服务段查看所有的交换器,你可以使用 rabbitmqctl命令:

sudo rabbitmqctl list_exchanges

在这个返回的结果列表中,将会有一些 amq.*名称的交换器和默认(未命名)的交换器。这些都是默认创建的,但是你不太可能在这个时候使用它们。

默认交换器

在之前的章节中我们对交换器一无所知,但是却仍然可以给队列发送消息。那是因为我们使用默认的交换器,并且是由空字符串""来标识的。

回顾一下我们之前是怎么发布一条消息的:

channel.sendToQueue('hello', Buffer.from('Hello World!'));

这里我们使用了默认的或者是未命名的(匿名的)交换器:指定第一个参数为交换器的具体名称并将消息路由到队列,如果第一个参数存在的话。

现在,我们可以发布消息给我们已命名的交换器:

channel.publish('logs', '', Buffer.from('Hello World!'));

使用空字符串作为第二个参数意味着我们不想将消息发送到任何特定的队列。我们只想将其发布到我们的”logs“交换器中。

临时队列(temporary queues)

你也许会想到之前我们使用了有特殊名称的队列(像hellotask_queue?)。能够命名队列对我们来说是非常重要的——我们需要将多个工人指向同一个队列。当你想要在生产者和消费者中间分享一个队列的时候,提供一个队列的名称是非常重要的。

但是这并不是我们日志系统的情况。我们希望知道所有的日志消息,不仅仅只是所有日志消息中的一部分(下面都叫做子集合)。我们也对当前流动的消息感兴趣而不是旧的消息。为了解决这个问题,我们需要两个东西。

首先,不管何时我们连接到Tabbit,我们都需要一个新的,空的队列。为此,我们可以使用随机名称来创建队列,或者——我们可以让服务器来为我们选择一个随机的队列名称是更好的。

其次,一旦我们断开了消费者的连接,队列也应该被自动删除。

amqp.node客户端中,当我们将队列名称命名为一个空字符串的时候,我们会创建一个自动生成名称且非持久的队列:

channel.assertQueue('', {
  exclusive: true
});

当方法返回的时候,队列的实例中会包含一个由RabbitMQ生成的随机队列名称。例如,它看起来会像这样:amq.gen-JzTY20BRgKO-HjmUJj0wLg

当申明它的连接关闭时,由于它被声明为独享的(exclusive),所以队列将会被删除。你可以在guide on queues中了解exclusive标记的更多使用方法和其他队列属性。

连接(Bindings)

img

我们已经创建了一个fanout交换器和一个队列。现在我们需要告诉交换器给我们的队列发送消息。在交换器和队列的中间的这层关系我们叫binding

channel.bindQueue(queue_name, 'logs', '');

从现在开始,logs交换器将会给我们的队列添加消息。

查看连接器

你可以使用什么去查看所有的连接器呢?动动你的小脑筋猜猜。

rabbitmqctl list_bindings

把它们合到一起(putting it all together)

img

发布日志消息的生产者程序和之前的教程看起来并没有什么不同。最重要的变化就是我们现在想要向我们的logs交换器发布消息而不是匿名的交换器。当发送的时候我们需要一个提供路由键,但是它的值会被fanout交换器忽略掉。下面是emit_log.js脚本的代码:

#!/usr/bin/env node

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(error0, connection) {
  if (error0) {
    throw error0;
  }
  connection.createChannel(function(error1, channel) {
    if (error1) {
      throw error1;
    }
    var exchange = 'logs';
    var msg = process.argv.slice(2).join(' ') || 'Hello World!';

    channel.assertExchange(exchange, 'fanout', {
      durable: false
    });
    channel.publish(exchange, '', Buffer.from(msg));
    console.log(" [x] Sent %s", msg);
  });

  setTimeout(function() {
    connection.close();
    process.exit(0);
  }, 500);
});

(emit_log.js 源码)

正如你所看到的,在建立连接之后,我们定义了交换器。这一步是必要的,由于发布一个不存在的交换器是被禁止的。

如果还没有队列绑定到交换器,消息会丢失。但是对于我们来说是没问题的;如果还没有正在监听的消费者,我们可以安全的丢弃这条消息。

receive_logs.js的代码如下:

#!/usr/bin/env node

var amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(error0, connection) {
  if (error0) {
    throw error0;
  }
  connection.createChannel(function(error1, channel) {
    if (error1) {
      throw error1;
    }
    var exchange = 'logs';

    channel.assertExchange(exchange, 'fanout', {
      durable: false
    });

    channel.assertQueue('', {
      exclusive: true
    }, function(error2, q) {
      if (error2) {
        throw error2;
      }
      console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", q.queue);
      channel.bindQueue(q.queue, exchange, '');

      channel.consume(q.queue, function(msg) {
        if(msg.content) {
            console.log(" [x] %s", msg.content.toString());
          }
      }, {
        noAck: true
      });
    });
  });
});

(receive_logs.js 源码)

如果你想要保存日志到一个文件,只需要打开一个终端并输入:

./receive_logs.js > logs_from_rabbit.log

如果你希望在屏幕上看到日志输出,打开一个新的终端并运行:

./receive_logs.js

并且当然,也可以发布日志类型:

./emit_log.js

使用rabbitmqctl list_bindings你可以验证代码实际创建的连接和队列是否满足我们的预期。并且使用了两个终端来跑receive_logs.js程序,你将会在屏幕上看到这些:

sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs    exchange        amq.gen-JzTY20BRgKO-HjmUJj0wLg  queue           []
# => logs    exchange        amq.gen-vso0PVvyiRIL2WoV3i48Yg  queue           []
# => ...done.

这个结果阐释的很明显了:从logs交换器中出来的数据去到了两个服务器分别命名的队列中。并且这正是我们想要验证的。

如果想要弄清楚如何监听消息的子集,让我们继续学习第四章教程