rabbitMQ + yii2 (php)发布/订阅

在上篇文章中,我们搭建了一个工作队列。每个任务只分发给一个工作者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		[]

猜你喜欢

转载自blog.csdn.net/weixin_36851500/article/details/92853840