在上篇文章中,我们搭建了一个工作队列。每个任务只分发给一个工作者worker。这里我们做的跟之前完全不一样–分发一个消息给多个消费者consumers。这种模式 ”发布/订阅“。
在这里我们将会构建一个简单的日志系统。它包括两个程序–第一个程序负责发送日志消息,第二个程序负责发送获取消息并输出内容。
在我们的这个日志系统中,所有正在运行的接收方都会接收消息的。我们用其中一个接受者receiver把日志写入硬盘中,另一个接收者receiver把日志输出到屏幕上。
最终,日志消息被广播给所有的接收者。
接下来详细解析一下代码及实现的方式。
交换机 exchanges
在前面的学习中,我们发送消息到队列并从中取出消息。今天我们聊一下完整的消息模型。
在上次的学习中:
- 发布者 producer 是发布消息的应用程序。
- 队列 queue 用于消息存储的缓存
- 消费者 consumer 是接收消息的应用程序
MQ 消息模型的核心理念是:
- 发布者 producer 不会直接发送任何消息给队列,事实上,发布者 producer 甚至不知道消息是否已经被投递到队列。
- 发布者 producer 只需要把消费者发送至给一个交换机 exchange 。交换机是这样的,它一边从发布者放接收信息,一边把消息推送到队列。交换机必须知道如何处理它接收到的信息。是应该推送到指定队列还是多个队列,或者直接忽略消息。这些规则是通过交换机类型exchange type 来定义的。
有几个可供选择的交换机类型:
-
直连交换机 direct(默认)
直连交换机是一种带路由功能的交换机,要求改消息与一个特定的路由键完全匹配,这是一个完整的匹配,如果一个队列绑定到该交换机上要求路由键”dog“,只有被标记为”dog“的消息才被转发。不会转发”dog.puppy“,只会转发dog.(一对一匹配才转发) -
主题交换机 topic
它将路由键和某模式进行匹配,此时队列需要绑定一个模式上。符号”#’匹配一个或多个词,符号“‘匹配不多不少一个词。因此”audit.#“能够匹配到”audit.irs.corporate“ ,但是”audit.*“ 只能匹配到“audit.irs”。(匹配才会妆转发) -
头交换机 headers
它是忽略routing_keyd的一种路由方式,路由器和交换机路由的规则是通过Headers。将一个交换机声明成首部交换机,绑定一个队列的时候,定义一个Hash的数据结构,消息发送的时候,会携带一组Hash数据结构的信息,当Hash的内容匹配上的时候,消息就会被写入队列。 -
扇形交换机 fanout
扇形交换机是基本的交换机类型,他所能做的时期非常简单–广播消息,它会把能接收到的消息全部发送给绑定在自己身上的队列。因为广播不需要思考。所以它处理消息的速度也是所有的交换机类型里面最快的。
我们在这里主要一起学习下 – 扇形交换机 fanout 。先创建一个fanout类型的交换机,命名为logs:
//先创建一个fanout类型的交换机
$channel->exchange_declare('logs','fanout',false,false,false);
交换器列表
rabbitmqctl能够列出服务器上所有的交换器:
sh-3.2# rabbitmqctl list_exchanges
Listing exchanges for vhost / ...
name type
logs fanout
amq.rabbitmq.trace topic
amq.match headers
amq.direct direct
direct
amq.headers headers
amq.fanout fanout
amq.topic topic
log fanout
这些列表中有一些叫做amq.*的交换器,这些都是默认创建的。不过这时候我们还不需要它。
匿名的交换器
前面的学习中,我们对交换机一无所知,但仍然能够发送消息到队列中,因为我们使用了命名为空字符串(“”)默认的交换机。
回想我们之前是如何发布一则消息:
$channel->basic_publish($msg, 'logs');
这里我们使用默认或匿名交换机:
消息将会根据指定的routing_key分发到指定的队列,routing_key是basic_publish函数的第二个参数
我们就可以发送消息到一个我们命名的交换机:
$channel->exchange_declare('logs', 'fanout', false, false, false);
$channel->basic_publish($msg, 'logs');
临时队列
你还记得之前我们使用的队列名称吗?给一个队列命名是很重要的。我们需要把工作者workers指向正确的队列,如果你打算在发布者producers和消费者consumers之间共享队列的话,给队列命名是十分重要的。
但是这并不适用于我们的日志系统,我们打算接收所有的日志消息,而不仅仅 是接收一小部分,我们关心的是最新的消息而不是最旧的,为了解决这个问题,我们需要做两件事。
1、当我们链接MQ时,我们需要一个全新的、空的队列。我们可以动手创建一个随机的队列名。或者让服务器为我们选择一个随机的队列名。
2、当与消费者断开链接的时候,这个队列应当被立即删除
3、在php-amqplib 客户端,当我们提供队列名称为空字符串时,我们创建了一个具有生成名称的非持久性队列:
list($queue_name, ,) = $channel->queue_declare("");```
方法返回时,$queue_name变量包含一个随机生成的RabbitMQ队列名称。例如,类似amq.gen-jzty20brgko-hjmujj0wlg。
绑定 bindings
我们已经创建了一个扇形交换机 fanout 和一个队列,现在我们需要告诉交换机如何发送消息给我们的队列,交换器和队列之间的联系我们称为绑定binding。
$channel->queue_bind($queue_name, 'logs');
列出所有现存的绑定
rabbitmqctl list_bindings
sh-3.2# rabbitmqctl list_bindings
Listing bindings for vhost /...
source_name source_kind destination_name destination_kind routing_key arguments
exchange amq.gen-DIEEqc42QzI-SRg1D0Sv6A queue amq.gen-DIEEqc42QzI-SRg1D0Sv6A []
logs exchange amq.gen-DIEEqc42QzI-SRg1D0Sv6A queue []
代码整合
发布日志消息的程序看起来和之前的没有太大的区别,最重要的改变就是我们把消息发送给logs交换机而不是匿名交换机,在发送的时候我们需要提供routong_key参数,但是他的值会被扇形交换机 fanout exchange 忽略。以下是actionEmitlog脚本
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* 生产者 发送信息
*/
namespace yii\console\controllers;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\test\FixtureTrait;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
/**
* Manages fixture data loading and unloading.
*
* ```
* #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
* yii fixture/load User
*
* #also a short version of this command (generate action is default)
* yii fixture User
*
* #load all fixtures
* yii fixture "*"
*
* #load all fixtures except User
* yii fixture "*, -User"
*
* #load fixtures with different namespace.
* yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
* ```
*
* The `unload` sub-command can be used similarly to unload fixtures.
*
* @author Mark Jebri <[email protected]>
* @since 2.0
*/
class ProducerController extends Controller
{
private $channel;
private $connection;
public function init ()
{
$amqp = yii::$app->params['amqp'];
//建立一个到RabbitMQ服务器的连接
$this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]);
$this->channel = $this->connection->channel();
}
/**
* 发送日志信息
*/
public function actionEmitlog(){
try {
//设置和发送者是一样的,我们打开一个连接和一个通道,然后声明我们将要消耗的队列。请注意,这与发送的队列中的队列相匹配。
//建立一个到RabbitMQ服务器的连接
$connection = $this->connection;
$channel = $this->channel;
//先创建一个fanout类型的交换机
$channel->exchange_declare('logs','fanout',false,false,false);
//接收数据
$parms = func_get_args();
//处理数据
$data = implode(' ',array_slice($parms,0));
if(empty($data)) $data = "info: Hello World!";
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'logs');
echo " [x] Sent ", $data, "\n";
$channel->close();
$connection->close();
} catch(\Exception $e) {
echo $e->getMessage();
}
}
}
正如你们看到的一样,在链接成功之后,我们声明了一个交换器,这一个是很重要的,因为不允许发布消息到不存在的交换器。
如果没有绑定队列到交换器,消息将会丢失,但这个没有所谓,如果没有消费者监听,那么消息就会被忽略。
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
* 消费者 接收信息
*/
namespace yii\console\controllers;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\console\Controller;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\test\FixtureTrait;
use common\tools\Pusher;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
/**
* Manages fixture data loading and unloading.
*
* ```
* #load fixtures from UsersFixture class with default namespace "tests\unit\fixtures"
* yii fixture/load User
*
* #also a short version of this command (generate action is default)
* yii fixture User
*
* #load all fixtures
* yii fixture "*"
*
* #load all fixtures except User
* yii fixture "*, -User"
*
* #load fixtures with different namespace.
* yii fixture/load User --namespace=alias\my\custom\namespace\goes\here
* ```
*
* The `unload` sub-command can be used similarly to unload fixtures.
*
* @author Mark Jebri <[email protected]>
* @since 2.0
*/
class ConsumerController extends Controller
{
private $channel;
private $connection;
public function init ()
{
$amqp = yii::$app->params['amqp'];
//建立一个到RabbitMQ服务器的连接
$this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]);
$this->channel = $this->connection->channel();
}
/**
* 接收信息
*/
public function actionReceivelogs(){
try{
//设置和发送者是一样的,我们打开一个连接和一个通道,然后声明我们将要消耗的队列。请注意,这与发送的队列中的队列相匹配。
//建立一个到RabbitMQ服务器的连接
$connection = $this->connection;
$channel = $this->channel;
//先创建一个fanout类型的交换机
$channel->exchange_declare('logs','fanout',false,false,false);
//随机创建信道
list($queue_name, ,) = $channel->queue_declare("",false,false,true,false);
//绑定交换机
$channel->queue_bind($queue_name,'logs');
echo ' [*] Waiting for logs. To exit press CTRL+C', "\n";
//回调函数
$callback = function($msg){
echo ' [x] ', $msg->body, "\n";
};
//接收信息
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while(count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
} catch(\Exception $e){
echo $e->getMessage();
}
}
}
运行命令
如果你想在屏幕中查看日志,那么打开一个新的终端然后运行
gongzgiyangdeMacBook-Air:yii2advanced gongzhiyang$ ./yii rabbitmq-consumer/receivelogs
[*] Waiting for logs. To exit press CTRL+C
^C
gongzgiyangdeMacBook-Air:yii2advanced gongzhiyang$ ./yii rabbitmq-consumer/receivelogs
[*] Waiting for logs. To exit press CTRL+C
[x] info: Hello World!
[x] info: Hello World!
[x] info: Hello World!
当然还要发送日志:
gongzgiyangdeMacBook-Air:yii2advanced gongzhiyang$ ./yii rabbitmq-producer/emitlog
[x] Sent info: Hello World!
使用rabbitmqctl list_bindings你可确认已经创建的队列绑定。
sh-3.2# rabbitmqctl list_bindings
Listing bindings for vhost /...
source_name source_kind destination_name destination_kind routing_key arguments
exchange amq.gen-DIEEqc42QzI-SRg1D0Sv6A queue amq.gen-DIEEqc42QzI-SRg1D0Sv6A []
exchange amq.gen-VSVBnl566n2nCqIET7soZA queue amq.gen-VSVBnl566n2nCqIET7soZA []
logs exchange amq.gen-DIEEqc42QzI-SRg1D0Sv6A queue []
logs exchange amq.gen-VSVBnl566n2nCqIET7soZA queue []