在上一個教程中,我們創建了一個工作隊列。工作隊列背後的假設是每個任務只傳遞給一個工人。在這一部分中,我們將做一些完全不同的事情——我們將向多個消費者傳遞一個消息。這種模式被稱爲“發佈/訂閱”。
爲了說明這種模式,我們將構建一個簡單的日誌系統。它將由兩個程序組成——第一個程序將發出日誌消息,第二個程序將接收並打印它們。
在我們的日誌系統中,接收器程序的每個運行副本都會收到消息。這樣,我們就可以運行一個接收器並將日誌定向到磁盤;同時,我們還可以運行另一個接收器並在屏幕上查看日誌。
本質上,發佈的日誌消息將被廣播到所有接收者。
交換機
在本教程的前面部分中,我們向隊列發送和接收消息。現在是時候在Rabbit中引入完整的消息傳遞模型了。
讓我們快速回顧一下之前教程中介紹的內容:
- 生產者是發送消息的用戶應用程序。
- 隊列是存儲消息的緩衝區。
- 消費者是接收消息的用戶應用程序。
RabbitMQ中消息傳遞模型的核心思想是,生產者從不直接向隊列發送任何消息。實際上,生產者常常根本不知道消息是否會被傳遞到任何隊列。
相反,生產者只能向交換機發送消息。交換機是一個很簡單的東西。一方面它接收來自生產者的消息,另一方面它將消息推送到隊列中。交換機必須確切知道如何處理它接收到的消息。是否應該將其附加到特定隊列?是否應該將其附加到多個隊列中?或者應該被丟棄。其規則由交換機類型定義。
有幾種交換機類型可用:direct、topic、headers和fanout。我們將集中討論最後一個——fanout扇形。讓我們創建一個這種類型的交換,並將其稱爲logs:
channel.exchangeDeclare("logs", "fanout");
扇型交換機非常簡單。正如您可能從名稱中猜到的,它只是將它接收到的所有消息廣播到它知道的所有隊列。這正是我們需要的日誌記錄器。
列出所有交換機
要列出服務器上的交換,可以運行非常有用的rabbitmqctl:
- sudo rabbitmqctl list_exchanges**
在此列表中,將有一些amq.*交換和默認(未命名)交換。這些都是默認創建的,但現在不太可能需要使用它們。
無名交換機
在本教程的前幾部分中,我們對交換一無所知,但仍然能夠向隊列發送消息。這是可能的,因爲我們使用的是默認的交換,我們用空字符串(“”)標識它。
回想一下我們之前是如何發佈消息的:
- channel.basicPublish("", “hello”, null, message.getBytes());
第一個參數是交換的名稱。空字符串表示默認或無名稱的交換機:消息被路由到具有routingKey指定的名稱的隊列(如果存在的話)。
現在,我們可以發佈到指定的交易所:
channel.basicPublish( "logs", "", null, message.getBytes());
臨時隊列
您可能還記得,我們使用的隊列具有特定的名稱(還記得hello和task_queue嗎?)。能夠命名隊列對我們來說至關重要——我們需要將消費者指向同一個隊列。當您想在生產者和消費者之間共享隊列時,給隊列一個名稱非常重要。
但我們的日誌系統不是這樣。我們希望瞭解所有日誌消息,而不僅僅是其中的一個子集。我們也只對當前的消息感興趣,而不是舊的消息。要解決這個問題,我們需要兩件事。
首先,當我們連接到Rabbit時,我們需要一個新的、空的隊列。爲此,我們可以創建一個隨機名稱的隊列,或者更好的方法是讓服務器爲我們選擇一個隨機隊列名稱。
其次,一旦我們斷開消費者的連接,隊列就會自動刪除。
在Java客戶端中,當我們不向queueDeclare()提供參數時,我們將創建一個非持久的、排他的、自動刪除的隊列,並生成一個名稱:
String queueName = channel.queueDeclare().getQueue();
您可以在隊列指南中瞭解有關獨佔標誌和其他隊列屬性的更多信息。
此時,queueName包含一個隨機隊列名。例如它看起來像amq.gen-JzTY20BRgKO-HjmUJj0wLg。
綁定
我們已經創建了扇型交換機和隊列。現在我們需要告訴交換機向我們的隊列發送消息。交換和隊列之間的關係稱爲綁定。
channel.queueBind(queueName, "logs", "");
從現在起,logs交換機將向我們的隊列追加消息。
列出綁定
你可以列出正在使用的綁定關係,你猜到的,
- rabbitmqctl list_bindings
把它們放在一起
發送日誌消息的生產者程序,與上一個教程的日誌看起來沒什麼不同。最重要的變化是,我們現在希望將消息發佈到logs交換機,而不是匿名的交換機。我們需要在發送時提供routingKey,但它的值在扇型交換中被忽略。代碼在這裏EmitLog.java:
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" :
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
如您所見,在建立連接之後,我們聲明瞭交換機。此步驟是必需的,因爲禁止發佈消息到不存在的交換機。
如果還沒有隊列綁定到交換機,那麼消息將丟失;如果沒有消費者在監聽,我們可以安全地丟棄消息。
代碼ReceiveLogs.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
像以前一樣編譯
javac -cp $CP EmitLog.java ReceiveLogs.java
如果要將日誌保存到文件中,只需打開控制檯並鍵入:
java -cp $CP ReceiveLogs > logs_from_rabbit.log
如果希望在屏幕上查看日誌,請打開一個新的終端並運行:
java -cp $CP ReceiveLogs
當然,要發送日誌消息:
java -cp $CP EmitLog
使用rabbitmqctl list_bindings,您可以驗證代碼是否實際創建了所需的綁定和隊列。如果有兩個ReceiveLogs.java正在運行的程序應該會看到如下內容:
sudo rabbitmqctl list_綁定
sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
# => logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
# => ...done.
對結果的解釋很簡單:來自logs交換機的數據通過服務器分配的名稱進入兩個隊列。這正是我們想要的。
要了解如何監聽消息的子集,讓我們繼續學習教程4