在上一篇中,我们创建了一个工作队列,在该队列中我们都是假设一条消息会被分配到某个具体的工作进程(消费者)。在本篇中我们将讨论一种完全不同的方式:将一条消息分发同时分发给多个消费者,这种模式就是众所周知的"发布/订阅"。为了研究该模式,我们将创建一个简单的日志系统。该日志系统分类两部分:第一部分记录日志,第二部分则负责接受和打印这些日志。在日志系统中,接收者的每个运行副本都能接受消息。这种模式下我们通过一个接收器将日志刷新到磁盘,同时运行另一个接收器查看日志内容。最终日志消息就被广播给了所有接收者。

交换机

在之前的篇章中发送接收消息都是在一个队列中玩,现在有必要完整的讨论一下Rabbit中的消息队列模型。在此之前先简单回顾下前面的篇章,我们主要讨论了这些概念:

  • 生产者:负责发送消息的应用程序
  • 队列:存储消息的缓冲区
  • 消费者:负责接收消息的应用程序

RabbitMQ消息队列的核心思想是生产者不直接发送任何消息到队列中,此时生产者甚至不知道一条消息是否会被发送到一个队列中。生产唯一知道的是消息发送给了一台交换机。交换机本身是一个相对简单的结构,它的一端负责接收生产发送的消息,另一端负责将消息投递到消息队列中。交换机通常具有这样的特点:确切的知道接收到的每条消息的功能;该消息是添加到特定的队列还是添加到多个队列;是否丢弃该消息。

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_php

 在RabbitMQ中这些规则都是通过 exchange type来定义的,常用包含 direct  topic  headers  fanout 。通过rabbitmqctl list_exchanges查看所有的交换机。

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_publish/subscribe_02

本篇中我们主要讨论 fanout 我们先创建一个名为logs的队列:

//声明一个名字为 logs 的队列
$queue_name = 'logs';
$channel->queue_declare($queue_name, 'fanout', true, false, false);

fanout是一种相对简单的交换机。从名字上可以大致猜出,它会将它接收到的消息广播到它知道的队列中。而这种特性正是我们需要的。其实之前的案例中我们已经接触过交换机,只是没有明确提出来。记得我们发送消息的代码是长这样的:

//发送消息到队列中
$channel
->basic_publish($msg, '', 'hello');

我们之前的用的其实是默认交换机,也就是代码中的那个第二个参数''。如果使用默认交换机或匿名交换机,消息就会被路由到具体的消息队列中,也就是第三个参数指定的那个队列中。现在我们用交换机来改造我们之前的生产者:

//声明一个名字为logs的交换机
$exchange_name = 'logs';
$channel->exchange_declare($exchange_name, 'fanout', true, false, false);
...
//发送消息
$channel->basic_publish($msg, $exchange_name);

临时队列

之前我们的使用的队列都是具名的队列(hello,hello-durable)。队里的名称对我们来说非常的重要,我们需要将消费者归属到同一个队列中。当我们需要在生产者和消费者之间共享一个队列时,队列名称就显得异常的重要。但是这些在日志系统中就不在那么重要。我们需要监听所有的消息,而不仅仅是归类消息。我们更关注当前跟踪的消息而不是那些历史消息。为了实现这个目标,我们需要两件条件:

首先 无论什么时候连接到Rabbit,我们都需要一个全新的空队列。为了实现这个,我们需要为队列生成一个随机名称或者更好的选择是让服务器为队列选择一个随机名称。

第二,一旦消费者从断开连接,队列需要自动被回收。

list($queue_name, ,) = $channel->queue_declare("");

通过以上方式,RabbitMQ会返回一个随机队列名称。类似 amq.gen-_9F8T5RbPW-jgqtzlSHcKw 。当声明的连接关闭,队列将被删除,因为这些队列为独占(exclusive)模式。

绑定

我们创建了一个fanout交换机和一个队列。现在我们需要通知交换机发送信息到队列中。交换机和队列之间的关系被称为绑定。

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_php_03

代码实现:

$channel->queue_bind($queue_name, 'logs');

现在交换机将添加消息到队列中 

查看绑定列表

rabbitmqctl list_bindings

聚合

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_日志系统_04

 现在我们需要将相关部分组装起来,就像上图这样。生产者程序和前篇中没有发生太多变化,最主要的变化就是将消息发布到具名的logs交换机中,而不是匿名交换机中。代码实现:

//生产者
<?php require_once "./vendor/autoload.php"; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Message\AMQPMessage; //因为笔者本地安装了多个RabbitMQ 随机选择了5673端口的(RabbitMQ默认TCP端口5672, 用户名和密码为:guest) $connect = new AMQPStreamConnection("localhost", 5673, "guest", "guest"); //创建频道 内部API通过该方式进行交互【具体负责和MQ教育的对象】 $channel = $connect->channel(); //声明一个名字为 logs 的交换机 $exchange_name = 'logs'; $channel->exchange_declare($exchange_name, 'fanout', false, false, false); //从控制台接受消息 $message = implode(' ', array_slice($argv, 1)); if (empty($message)) { $message = '你好 MQ'; } //创建MQ消息体 【要通过MQ发送消息 当然就需要遵从MQ规定的消息格式(即所谓的通讯协议)】 $msg = new AMQPMessage($message, ['delivery_mode'=>AMQPMessage::DELIVERY_MODE_PERSISTENT]); //发送消息 $channel->basic_publish($msg, $exchange_name); echo 'Oj8k 我们已经将消息发送到了MQ中'; //销毁相关对象 $channel->close(); $connect->close();

在建立连接后我么就声明了交换机,这一步非常重要,因为禁止发布消息到一个不存在的交换机中。如果队列没有绑定到交换机中,那么消息就会丢失。但是对于我们来说也能接受。如果没有消费者监听,我们可以安全的丢弃消息。

//消费者
<?php
require_once './vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connect = new AMQPStreamConnection('localhost', 5673, 'guest', 'guest');
$channel = $connect->channel();

//开启负载均衡
$channel->basic_qos(null, 1, null);

//声明交换机
$exchange_name ='logs';
$channel->exchange_declare($exchange_name, 'fanout', false, false, false);

list($queue_name, , )= $channel->queue_declare("", false, true, true, false);

//绑定队列到交换机中
$channel->queue_bind($queue_name, $exchange_name);

$channel->basic_consume($queue_name, '', false, true, false, false, function ($msg) {
    echo '收到消息' . $msg->body . PHP_EOL;
    $time = substr_count($msg->body, '.');
    sleep($time);
    echo '任务处理完成';
});

while($channel->is_open()) {
    $channel->wait();
}

$channel->close();
$connect->close();

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_发送消息_05

 通过 rabbitmqctl list_building 我们也可以校验交换机是否是我们需要的:

第003篇 RabbitMQ - 发布订阅(publish/subscribe)_日志系统_06

 数据从交换机logs进入两个由服务器分配名称的队列。这正是我们要实现的效果。

名为小兵 不忘初心