發佈/訂閱
(使用pika 0.9.5 Python客戶端)
在上篇教程中,我們搭建了一個工作隊列,每個任務只分發給一個工作者(worker)。在本篇教程中,我們要做的跟之前完全不一樣 —— 分發一個消息給多個消費者(consumers)。這種模式被稱爲“發佈/訂閱”。
爲了描述這種模式,我們將會構建一個簡單的日誌系統。它包括兩個程序——第一個程序負責發送日誌消息,第二個程序負責獲取消息並輸出內容。
在我們的這個日誌系統中,所有正在運行的接收方程序都會接受消息。我們用其中一個接收者(receiver)把日誌寫入硬盤中,另外一個接受者(receiver)把日誌輸出到屏幕上。
最終,日誌消息被廣播給所有的接受者(receivers)。
交換機(Exchanges)
前面的教程中,我們發送消息到隊列並從中取出消息。現在是時候介紹RabbitMQ中完整的消息模型了。
讓我們簡單的概括一下之前的教程:
- 發佈者(producer)是發佈消息的應用程序。
- 隊列(queue)用於消息存儲的緩衝。
- 消費者(consumer)是接收消息的應用程序。
RabbitMQ消息模型的核心理念是:發佈者(producer)不會直接發送任何消息給隊列。事實上,發佈者(producer)甚至不知道消息是否已經被投遞到隊列。
發佈者(producer)只需要把消息發送給一個交換機(exchange)。交換機非常簡單,它一邊從發佈者方接收消息,一邊把消息推送到隊列。交換機必須知道如何處理它接收到的消息,是應該推送到指定的隊列還是是多個隊列,或者是直接忽略消息。這些規則是通過交換機類型(exchange type)來定義的。
有幾個可供選擇的交換機類型:直連交換機(direct), 主題交換機(topic), (頭交換機)headers和 扇型交換機(fanout)。我們在這裏主要說明最後一個 —— 扇型交換機(fanout)。先創建一個fanout類型的交換機,命名爲logs:
channel.exchange_declare(exchange='logs',
type='fanout')
扇型交換機(fanout)很簡單,你可能從名字上就能猜測出來,它把消息發送給它所知道的所有隊列。這正是我們的日誌系統所需要的。
交換器列表
rabbitmqctl能夠列出服務器上所有的交換器:
$ sudo rabbitmqctl list_exchanges Listing exchanges ... logs fanout amq.direct direct amq.topic topic amq.fanout fanout amq.headers headers ...done.
這個列表中有一些叫做amq.*的交換器。這些都是默認創建的,不過這時候你還不需要使用他們。
匿名的交換器
前面的教程中我們對交換機一無所知,但仍然能夠發送消息到隊列中。因爲我們使用了命名爲空字符串("")默認的交換機。
回想我們之前是如何發佈一則消息:
channel.basic_publish(exchange='', routing_key='hello', body=message)
exchange參數就是交換機的名稱。空字符串代表默認或者匿名交換機:消息將會根據指定的routing_key分發到指定的隊列。
現在,我們就可以發送消息到一個具名交換機了:
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
臨時隊列
你還記得之前我們使用的隊列名嗎( hello和task_queue)?給一個隊列命名是很重要的——我們需要把工作者(workers)指向正確的隊列。如果你打算在發佈者(producers)和消費者(consumers)之間共享同隊列的話,給隊列命名是十分重要的。
但是這並不適用於我們的日誌系統。我們打算接收所有的日誌消息,而不僅僅是一小部分。我們關心的是最新的消息而不是舊的。爲了解決這個問題,我們需要做兩件事情。
首先,當我們連接上RabbitMQ的時候,我們需要一個全新的、空的隊列。我們可以手動創建一個隨機的隊列名,或者讓服務器爲我們選擇一個隨機的隊列名(推薦)。我們只需要在調用queue_declare方法的時候,不提供queue參數就可以了:
result = channel.queue_declare()
這時候我們可以通過result.method.queue獲得已經生成的隨機隊列名。它可能是這樣子的:amq.gen-U0srCoW8TsaXjNh73pnVAw==。
第二步,當與消費者(consumer)斷開連接的時候,這個隊列應當被立即刪除。exclusive標識符即可達到此目的。
result = channel.queue_declare(exclusive=True)
綁定(Bindings)
我們已經創建了一個扇型交換機(fanout)和一個隊列。現在我們需要告訴交換機如何發送消息給我們的隊列。交換器和隊列之間的聯繫我們稱之爲綁定(binding)。
channel.queue_bind(exchange='logs',
queue=result.method.queue)
現在,logs交換機將會把消息添加到我們的隊列中。
綁定(binding)列表
你可以使用
rabbitmqctl list_bindings
列出所有現存的綁定。
代碼整合
發佈日誌消息的程序看起來和之前的沒有太大區別。最重要的改變就是我們把消息發送給logs交換機而不是匿名交換機。在發送的時候我們需要提供routing_key參數,但是它的值會被扇型交換機(fanout exchange)忽略。以下是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()
(emit_log.py 源文件)
正如你看到的那樣,在連接成功之後,我們聲明瞭一個交換器,這一個是很重要的,因爲不允許發佈消息到不存在的交換器。
如果沒有綁定隊列到交換器,消息將會丟失。但這個沒有所謂,如果沒有消費者監聽,那麼消息就會被忽略。
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()
(receive_logs.py source)
這樣我們就完成了。如果你想把日誌保存到文件中,只需要打開控制檯輸入:
$ python receive_logs.py > logs_from_rabbit.log
如果你想在屏幕中查看日誌,那麼打開一個新的終端然後運行:
$ python receive_logs.py
當然還要發送日誌:
$ python emit_log.py
使用rabbitmqctl
list_bindings
你可確認已經創建的隊列綁定。你可以看到運行中的兩個receive_logs.py程序:
$ sudo rabbitmqctl list_bindings
Listing bindings ...
...
logs amq.gen-TJWkez28YpImbWdRKMa8sg== []
logs amq.gen-x0kymA4yPzAT6BoC/YP+zw== []
...done.
顯示結果很直觀:logs交換器把數據發送給兩個系統命名的隊列。這就是我們所期望的。
如何監聽消息的子集呢?讓我們移步教程4