来源于官方手册(通俗易懂) https://www.rabbitmq.com/tutorials/tutorial-one-php.html
生产者:send.php
channel();
//为了发送,我们必须声明 一个队列queue 来发送
//声明队列 是幂等性的,队列只在它不存在的时候创建
$channel->queue_declare(
'hello',
false,
false,
false,
false
);
//消息的内容是 字节数组 格式,所以你可以 对其进行编码(encode)
$msg = new AMQPMessage('Hello World:');
$channel->basic_publish(
$msg,
'', //交换机名称
'hello' //路由key
);
echo " [x] Sent 'Hello World!'\n";
//关闭channel
$channel->close();
//关闭连接
$channel->close();
receive.php
channel();
//注意, 这里也声明了队列(queue),因为这个消费者启动的程序 可能 早于生产者程序的启动,
//在这里也声明为了确保,在我们试着从它里边消费时,这个队列是存在的。
$channel->queue_declare(
'hello',
false,
false,
false,
false
);
//记住,消息被server 异步发送给 client(消费端)的
//需要定义一个 回调函数, 参数是 消息
$callback = function ($msg) {
echo ' [x] Received ', $msg->body, "\n";
};
//
$channel->basic_consume(
'hello',
'',
false,
true,
false,
false,
$callback
);
//不论何时,我们接收到了消息,我们将调用 $callback 函数,参数为"消息"
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
在没有ack 情况下 像 一个任务 执行需要几秒钟,如果一个worker 被kill 或者其他服务器问题,这个worker 接收的这个消息被没有被正确处理, 而在队列里边 它已经被标记为for deletion。这样情况 这个消息就丢了
为了 确保 一个消息 永不丢失,RabbitMQ 支持 消息确认(message acknowledgments)
//如果一个 消费者 consumer 死掉了(他的channel 被关闭,connection被关闭,或者 tcp连接断了),这时它没有发送 一个 ack RabbitMQ 将理解为 这个消息没有被完全处理,将重新把它放到 队列里。如果这时候,有其他的 消费者在线,它将很快将此消息投递给其他的消费者。
注意:不存在 消息 超时时间,即便是 处理一个消息需要非常长非常长的时间。
这是常见的错误,后果很严重。由于它不能够释放任何没被确认的消息,它将占用越来越多的内存。
为了能够找出 这种类型 的错误。使用命令:
//sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
当 RabbitMQ quit 或者宕机,它会丢掉 队列或消息 除非你告诉它不要这样。
为了消息不丢失。 我们需要做2件事,标记队列 和 消息 都为 持久化(durable)
1. 这个 durable =true 的标志,需要生产者 和消费者 都去设置
2. 在AMQPMessage 中 设置 delivery_mode=2
标记消息为 持久化,不能够完全 保证消息不丢失。因为这里存在很短的时间窗口:当RabbitMQ收到消息还没有保存。 // RabbitMQ 不会针对每个消息 来进行 fsync, 他会先保存到 缓存里。 // 这种持久机制 不是强壮的,但是对于简单的任务队列是足够了。如果你需要一个更加强壮的保障机制, // 你可以使用 publisher_confirms.
【注意】:RabbitMQ 不允许你 使用不同的参数 去重新定义一个 已经存在的 队列queue,如果这样做了,它将返回一个错误error
默认地: RabbitMQ 它 不看 消费者的未确认消息的数量,它仅仅不加思考地 分发每一个 n-th 消息到 n-th 消费者。
为了改变这种分发策略! 可以使用 basic_qos 设置 prefetch_count=1;
// 含义是:不要分发 一个新消息给 一个消费者 除非它已经处理并确认了之前的一个消息。
$channel->basic_qos(null, 1, null);
new_task.php 代码如下:
channel();
//为了发送,我们必须声明 一个队列queue 来发送
//声明队列 是幂等性的,队列只在它不存在的时候创建
$channel->queue_declare(
'task_queue', //前一个 hello
false,
true, //持久化 false
false,
false
);
//消息的内容是 字节数组 格式,所以你可以 对其进行编码(encode)
$data = implode(' ', array_slice($argv, 1));
if (empty($data)) {
$data = "Hello World!";
}
$msg = new AMQPMessage(
$data,
array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT) //想让消息持久化必须进行此设置
);
$channel->basic_publish(
$msg,
'', //交换机名称
'task_queue' //路由key ; 之前为task_queue
);
echo " [x] Sent ", $data, "\n";
//关闭channel
$channel->close();
//关闭连接
$channel->close();
//默认地
// RabbitMQ 将 按照顺序地 发送每个消息 给 下一个 消费者;
// 平均每个消费者 将得到 相同数量的消息。这种分发消息的方式 叫 round-robin (轮询)
/**
* //运行结果 -shell1
[root@liang workQueues]# php new_task.php First message.
[x] Sent First message.
[root@liang workQueues]# php new_task.php Second message..
[x] Sent Second message..
[root@liang workQueues]# php new_task.php Third message...
[x] Sent Third message...
[root@liang workQueues]# php new_task.php Fourth message....
[x] Sent Fourth message....
[root@liang workQueues]# php new_task.php Fifth message.....
[x] Sent Fifth message.....
// shell2
[x] Received Second message..
[x] Done
[x] Received Fourth message....
[x] Done
// shell3
[x] Received First message.
[x] Done
[x] Received Third message...
[x] Done
[x] Received Fifth message.....
[x] Done
*
*
*/
worker.php 代码如下:
channel();
//注意, 这里也声明了队列(queue),因为这个消费者启动的程序 可能 早于生产者程序的启动,
//在这里也声明为了确保,在我们试着从它里边消费时,这个队列是存在的。
$channel->queue_declare(
'task_queue', //之前为hello
false,
true, //之前为false
false,
false
);
//假的任务 消耗的时间,一个点一秒钟
$callback = function ($msg) {
echo ' [x] Received ', $msg->body, "\n";
sleep(substr_count($msg->body, '.'));
echo " [x] Done\n";
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};
$channel->basic_qos(
null,
1, //不要分发 一个新消息给 一个消费者 除非它已经处理并确认了之前的一个消息。
null
);
//
$channel->basic_consume(
'task_queue', //之前为hello
'',
false,
false, //true 不确认; false 确认
false,
false,
$callback
);
//不论何时,我们接收到了消息,我们将调用 $callback 函数,参数为"消息"
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
投递一个消息给 多个消费者。
模式被叫做 发布/订阅 “publish/subscribe”
为了说明这个模式 。我们将 构建一个 简单的 日志系统。
发布日志 消息 将广播给 所有 消费者receivers
一个 消费者 worker 记录日志到 硬盘; 另一个消费者 worker 打印日志到 屏幕上。
RbbitMQ 的消息模式里面,生产者从来 不要直接发送 任何消息到一个 队列queue.
替代地, 生产者可以 发送消息 到 exchange.
exchange 决定 发送给哪个 队列。
exchange 的类型 有 direct, topic, headers , fanout.
列出 exchanges
sudo rabbitmqctl list_exchanges
default exchange 默认exchange
在之前的教程中,我们不知道exchanges,但是 仍然能够发送消息给 队列queues.
这个很可能是因为: 我们正在使用一个默认的 exchange, 它 被定义 使用 空字符串“”
1 。不论何时 我们连接到 Rabbit 我们需要一个 新的,空的 队列。为了做到这样,
我们可以创建一个队列,使用随机的名字,更好的方式是 让 rabbitmq server 来选择一个随机的名字
,我们来用。
2 。 一旦 我们 断开连接, 消费者队列 应该被 自动地删除。
list($queue_name, ,) = $channel->queue_declare("");
名字可能像:amq.gen-JzTY20BRgKO-HjmUJj0wLg
当声明它的连接 关闭时, queue 将被 关闭,因为 它被声明为 exclusive
已经创建了一个 fanout类型的 exchange 和 一个 queue, 现在
我们需要告诉 exchage 去 发送 消息 给 我们的 queue
$channel->queue_bind($queue_name, 'logs');
相关命令:
列出存在的绑定。
rabbitmqctl list_bindings
生产者代码:emit_log.php
channel();
$channel->exchange_declare('logs', 'fanout', false,false, false);
$data = implode('', array_slice($argv, 1));
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();
因为没有 消费者正在监听
消费者代码:receive_logs.php
channel();
$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 ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
如果你想保存日志 到文件:
php receive_logs.php > logs_from_rabbit.log
如果你想 在屏幕上 看到日志输出:
php receive_logs.php
生产者。
php emit_log.php
为了 解决 如何 去 监听 一个 消息的子集,看 教程4
在前面日志的系统的基础上,增加功能。
如: 只有 严重错误的 消息 才记录到 硬盘上,另一方面打印 所有的日志信息到控制台上。
绑定:
$channel->queue_bind($queue_name, 'logs');
可以简单地读: 此queue 对 exchange 上的消息 感兴趣
第三个参数: binding key
binding key 含义 取决于 exchange 类型
对于 fanout 类型 exchange。 我们可以简单的忽略掉 此参数(不传递)
Direct 类型 exchange
fanout 类型的 exchange ,这种不能给我们 更多的 灵活性,它只是 简单的广播
type =direct ; 只要queue 它的binding key 匹配上 消息的 routing key ,队列就能收到消息。
多绑定:
多个队列queue 使用 相同的 binding key。 如果这样来设计,那么它的行为就 类似 fanout。如下图:
生产日志/ 发射日志 (emitting logs)
改进后的日志系统 使用的 queue 结构如下:
生产者代码:emit_log_direct.log
channel();
$channel->exchange_declare('direct_logs', 'direct', false,false, false);
$severity = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'info';
$data = implode('', array_slice($argv, 2));
if (empty($data)) {
$data = "info: Hello World!";
}
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'direct_logs', $severity);
echo ' [x] Sent ',$severity, ":", $data, "\n";
$channel->close();
$connection->close();
消费者代码:receive_logs_direct.log
channel();
$channel->exchange_declare('direct_logs', 'direct', false, false, false);
list($queue_name, ,) = $channel->queue_declare("", false, false,true, false);
$severities = array_slice($argv, 1);
if (empty($severities)) {
file_put_contents('php://stderr', "Usage: $argv[0] [info] [warning] [error]\n");
exit(1);
}
//对 日志 各种级别 进行绑定
foreach($severities as $severity) {
$channel->queue_bind($queue_name, 'direct_logs', $severity);
}
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 ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
尽管使用 direct exchange 提高了 我们的系统,它仍然有限制, 它不能 基于多个准则来 路由。
像unix 的 syslog 工具,它 是基于 安全级别( info/warning/critical …) 和 设备来源 (auth/cron/kernel…)
比如 syslog 可以做到 仅 监听 来自 cron 的errors 信息 和 来自 kern 的所有log信息
我们的log系统 为了做到 像syslog 那样,我们需要 topic exchange
binding keys 有两个特别重要的情况:
星号* 代替一个单词
# 号 可以代替 0 或 多个单词。
routing key 由 3个单词 组成 speed.colour.species //速度。颜色。物种
我们创建3个 binding key:
Q1 是 用 binding key *.orange.*
Q2 的 binding key *.*.rabbit 和 lazy.#
一个消息 它的 routing key 被设置为 “quick.orange.rabbit” ,将被投递给 2个queue
消息“ lazy.orange.elephant ,也将被投递给2个queue
"quick.orange.fox" 将去到 第一个 queue
lazy.brown.fox 将去到 第二个queue
lazy.pink.rabbit" 将被投递给 第二个 queue 一次。即便是它 和 2个 binding key 都匹配
"quick.brown.fox" 不匹配任何binding ,所以它将被丢弃。
像 "orange" or "quick.orange.male.rabbit 是 一个单词或者 四个单词 ,不匹配任何 binding key ,将被丢弃
"lazy.orange.male.rabbit 尽管它有四个单词,将匹配 lazy.# 将去到 第二个 queue
注意:当一个 queue 绑定了 “#”,它将接收所有的消息,忽略 routing key ,就像 fanout exchange
当 binding key 中没有 * 和 #,那么 topic exchange 的行为就像 direct exchange
channel();
$channel->exchange_declare('topic_logs', 'topic', false, false, false);
$routing_key = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'anonymous.info';
$data = implode(' ', array_slice($argv, 2));
if (empty($data)) {
$data = "Hello World!";
}
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'topic_logs', $routing_key);
echo ' [x] Sent ', $routing_key, ':', $data, "\n";
$channel->close();
$connection->close();
消费者代码:receive_logs_topic.php
channel();
$channel->exchange_declare('topic_logs', 'topic', false, false, false);
list($queue_name, ,) = $channel->queue_declare("", false, false, true, false);
$binding_keys = array_slice($argv, 1);
if (empty($binding_keys)) {
file_put_contents('php://stderr', "Usage: $argv[0] [binding_key]\n");
exit(1);
}
foreach ($binding_keys as $binding_key) {
$channel->queue_bind($queue_name, 'topic_logs', $binding_key);
}
echo " [*] Waiting for logs. To exit press CTRL+C\n";
$callback = function ($msg) {
echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n";
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
如果 我想在 远端的 计算机上 运行 一个 函数 并等待函数的 返回结果 。 这种模式 通常叫做 RPC
例子 : service 只返回 数字。
Callback queue
消息的属性:
AMQP 0-9-1 协议预定义了 14个属性,大多数 很少用,有几个还是能用到的 如:
delivery_mode //标记消息是否是持久的
content_type //编码格式,通常使用json编码,如: application/json
reply_to //通常用来命名 一个 callback queue
correlation_id // 用于 关联 Rpc response 和 requests
correlation id
如果我们看到一个 unknown 的 correlation_id 值,我们可以安全地删除 这个消息。
你可能会问,为什么我们是忽略 unknown 消息,而不是 发出一个error 错误呢。
这是由于 在服务端存在 罕见的情况。
rpc server 发送完answer 后,还没来得及发送 ack message 给 request,这时候 rpc server 死掉了。
如果这样的事发生了,重启rpc server后 将再次处理request。
这就是为什么 client 必须能优雅地处理 重复的 响应,并且 RPC server的处理 也应该是幂等性的。
client 代码:rpc_client.php
connection = new AMQPStreamConnection(
'localhost',
5672,
'guest',
'guest'
);
$this->channel = $this->connection->channel();
list($this->callback_queue, ,) = $this->channel->queue_declare(
"",
false,
false,
true,
false
);
$this->channel->basic_consume(
$this->callback_queue,
'',
false,
true,
false,
false,
array(
$this,
'onResponse'
)
);
}
public function onResponse($rep)
{
if($rep->get('correlation_id') == $this->corr_id) {
$this->response = $rep->body;
}
}
public function call($n)
{
$this->response = null;
$this->corr_id = uniqid();
$msg = new AMQPMessage(
(string) $n,
array(
'correlation_id' => $this->corr_id,
'reply_to' => $this->callback_queue
)
);
$this->channel->basic_publish($msg, '', 'rpc_queue');
while(!$this->response) {
$this->channel->wait();
}
return intval($this->response);
}
}
$fibonacci_rpc = new FibonacciRpcClient();
$response = $fibonacci_rpc->call(30);
echo ' [.] Got ', $response, "\n";
server代码:
channel();
$channel->queue_declare('rpc_queue', false, false, false, false);
function fib($n)
{
if ($n == 0) {
return 0;
}
if ($n == 1) {
return 1;
}
return fib($n-1) + fib($n-2);
}
echo " [x] Awaiting RPC requests\n";
$callback = function($req) {
$n = intval($req->body);
echo '[.] fib(', $n, ")\n";
$msg = new AMQPMessage(
(string) fib($n),
array('correlation_id' => $req->get('correlation_id'))
);
$req->delivery_info['channel']->basic_public(
$msg,
'',
$req->get('reply_to')
);
$req->delivery_info['channel']->basic_ack(
$req->delivery_info['delivery_tag']
);
};
$channel->basic_qos(null, 1, null);
$channel->basic_consume('rpc_queue',
'',
false,
false,
false,
false,
$callback
);
while($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
对于client ,client 需要一个 网络来回 对于一个 单独的RPC request.
sudo rabbitmqctl list_queues
sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
列出 exchanges
sudo rabbitmqctl list_exchanges
列出存在的绑定。
rabbitmqctl list_bindings