发布/订阅
前面的教程中我们已经创建了一个工作队列。在一个工作队列背后的假设是每个任务恰好会传递给一个工人。在这一部分里我们会做一些完全不同的东西——我们会发送消息给多个消费者。这就是所谓的“发布/订阅”模式。
为了解释这种模式,我们将会构建一个简单的日志系统。它包含两个程序——第一个产生日志消息,第二个接收并把他们打印出来。
在我们的日志系统中,每一个接收程序的正在运行的拷贝都会获知消息,那样我们将能够运行一个接收者把日志指向磁盘;同时我们将能够运行另一个接收者在屏幕上查看日志。
实质上,被发布的消息将会广播给所有接收。
交易所
在前面的教程中我们向一个队列中发送和接收消息. 现在来介绍Rabbit中的完整的消息模型 .
我们快速回顾一下前面的教程:
- 生产者是一个发送消息的用户应用。
- 队列是一个存储消息的缓冲区。
- 消费者是一个接收消息的用户应用。
RabbitMQ中消息模型的核心思想是,生产者从不直接发送任何消息给一个队列。实际上,生产者通常甚至一点都不知道一条消息会被发送给什么队列。
相反,生产者只能发送消息给一个交易所(exchange)。交易所是一个很简单的东西。它一面接收来自生产者的消息,一面把消息推送给队列。交易所必须准确知道如何对待它接收到的消息。它该被追加到一个特定队列中吗?还是应该把它追加到多个队列?又或者它该被忽略?这些规则都是由交易类型(exchange type)所定义的。
有几种可用的交易类型: direct, topic, headers 和 fanout. 我们会关注最后一个——fanout. 我们来创建一个这种类型的交易所,命名为 logs:
channel.exchange_declare(exchange='logs',
type='fanout')
- 1
- 2
fanout类型交易所非常简单。从名字中你可能已经猜到(其实英语非母语的也不大好猜到,fan(风扇)+out(外、出)的组合,我觉得在这里可以理解为扇出去、扬出去的意思),它只是广播它接到的所有消息给它知道的所有队列 ,这恰恰是我们的日志程序所需要的。
列出所有交际所
为了列出所有交易所,你可以运行Trabbitmqctl:
sudo rabbitmqctl list_exchanges 在这个列表中会有些像amq.* 的交易所和默认的(未命名的)交易所
这些是被默认创建的,但你目前不太可能需要用到它们。默认交易所
在之前的教程中我们队交易所一无所知。但仍然能够给队列发送消息。这是因为我们使用了默认交易所,我们通过空字符串 (“”)来标识。
回顾一下我们之前如何发布一条消息:
channel.basic_publish(exchange='',
routing_key='hello',
body=message)exchange 参数就是交易所的名字。空字符串代表默认或者无名交易所:
消息路由到名字被routing_key所指定的队列,如果队列存在的话。
现在我们可以把消息发布到命名交易所了:
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
- 1
- 2
- 3
临时队列
你可能记得我们先前使用了有特定名字的队列 (记得 hello 和 task_queue?)。能给队列命名对我们来说很关键——我们需要把工人们指向同一个队列。当你想要在生产者和消费者中分享队列时,给队列一个名字就很重要。
但那不符合我们的日志程序需要。我们想获取所有日志消息,不只是他们的一个子集。我们也仅对当前活跃的消息感兴趣,而不是旧有的,解决这个问题我们需要做两件事。
第一,无论何时连接到Rabbit我们都需要一个新鲜的(fresh)空的(empty)队列 ,为实现这一点我们使用随机的名字创建一个队列,或者更好的——让服务器随机选一个队列名给我们。 我们不给queue_declare提供队列参数就可以做到:
result = channel.queue_declare()
- 1
这样的话, result.method.queue 就会包含一个任意的队列名,例如它可能看上去是这样的 : amq.gen-JzTY20BRgKO-HjmUJj0wLg.
第二, 一旦我们断开消费者链接,队列就该被删除掉。通过一个exclusive 标志实现:
result = channel.queue_declare(exclusive=True)
- 1
绑定
我们已经创建了一个fanout 交易所和一个队列。现在我们要通知交易所发送消息给我们的队列。交易所和一个队列之间的关系叫做绑定。
channel.queue_bind(exchange='logs',
queue=result.method.queue)
- 1
- 2
从现在开始logs交易所会追加消息给我们的队列。
列出所有绑定
你可以列出所有存在的绑定,使用…好吧,你已经猜到了:
rabbitmqctl list_bindings
整合
用来发送日志消息的生产者程序,看起来和之前并没有多大不同。最大的变化是我们现在想要发送消息给logs交易所而不是无名交易所。当发送时我们需要提供一个routing_key,但对于fanout类型交易所来说它的值是被忽略的。 下面是emit_log.py 脚本代码:
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
type='fanout')
message = ' '.join(sys.argv[1:]) or "info: Hello World!"
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
print(" [x] Sent %r" % message)
connection.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
如你所见,建立完连接后我们生命了交易所。这一步是必须的,因为发布到一个不存在的交易所是被禁止的。
如果没有队列连接(bound)到交易所,消息就会丢失,但对我们来说没有关系;如果没有消费者监听我们可以安全地忽略掉这个消息。
receive_logs.py代码:
#!/usr/bin/env python
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
type='fanout')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='logs',
queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(callback,
queue=queue_name,
no_ack=True)
channel.start_consuming()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
大功告成!如果你想把日志保存到一个文件中只需要打开控制台,键入:
python receive_logs.py > logs_from_rabbit.log
- 1
如果你想在屏幕查看日志, 打开一个新的控制台运行:
python receive_logs.py
- 1
下两图分别为在我的Ubuntu终端使用cat命令输出的存入到日志文件的日志和屏幕显示的日志
图1
图2
当然需要运行emit_log.py
使用 rabbitmqctl list_bindings 你可以确认代码创建了绑定和队列,就像我们想要的那样。在两个运行的情况下你应该会看到如下内容:
sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
# => logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
# => ...done.
- 1
- 2
- 3
- 4
- 5
结果的解释很直白: 来自logs交易所的数据去往两个服务器指定了名字的队列 ,这正是我们所预想的。
若想知道如何监听消息子集,请前往下一节。