在上一篇的最後,編寫了一個C#驅動RabbitMQ的簡單栗子,瞭解了C#驅動RabbitMQ的基本用法。本章介紹RabbitMQ的四種Exchange及各種Exchange的使用場景。
1 direct型別
1 direct路由規則
上一篇最後一個栗子使用的Exchange就是direct型別的,direct型別的exchange路由規則很簡單:
exchange在和queue進行binding時會設定routingkey(為了避免和下邊的routingKey混淆,很多時候把這裡的routingKey叫做BindingKey)
channel.QueueBind(queue:"Q1", exchange:"myexchange", routingKey:"orange");
將訊息傳送到Broker時會設定對應的routingkey:
channel.BasicPublish(exchange: "myexchange",routingKey: "orange", basicProperties: null, body: body);
只有RoutingKey和BindingKey完全相同時,exchange才會把訊息路由到繫結的queue中去。
2 程式碼示例
我們知道了direact型別的交換機只有routingKey和bindingKey相同的時候才會進行訊息路由,根據這一特點我們可以通過routingKey將訊息路由到不同的queue中。如在進行日誌處理時,需求是所有的日誌都儲存到文字檔案,出現錯誤日誌時則還需要簡訊通知以便及時處理。我們可以建立兩個佇列:只接收錯誤日誌的log_error佇列和接收所有日誌資訊的log_all佇列。消費者C1處理log_error佇列中訊息,將這些訊息通過簡訊通知管理員,消費者C2處理log_all佇列的資訊,將這些資訊記錄到文字檔案。
生產者用於傳送日誌訊息,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myexchange", type: ExchangeType.Direct, durable: true, autoDelete: false, arguments: null); //宣告兩個佇列,log_all儲存所有日誌,log_error儲存error型別日誌 channel.QueueDeclare(queue: "log_all", durable: true, exclusive: false, autoDelete: false, arguments: null); channel.QueueDeclare(queue: "log_error", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結所有日誌型別到log_all佇列 string[] logtypes = new string[] { "debug", "info", "warn", "error" }; foreach (string item in logtypes) { channel.QueueBind(queue: "log_all", exchange: "myexchange", routingKey: item); } //繫結錯誤日誌到log_all佇列 channel.QueueBind(queue: "log_error", exchange: "myexchange", routingKey: "error"); //準備100條測試日誌資訊 List<LogMsg> msgList = new List<LogMsg>(); for (int i = 1; i < 100; i++) { if (i%4==0) { msgList.Add(new LogMsg() { LogType = "info", Msg = Encoding.UTF8.GetBytes($"info第{i}條資訊") }); } if (i % 4 == 1) { msgList.Add(new LogMsg() { LogType = "debug", Msg = Encoding.UTF8.GetBytes($"debug第{i}條資訊") }); } if (i % 4 == 2) { msgList.Add(new LogMsg() { LogType = "warn", Msg = Encoding.UTF8.GetBytes($"warn第{i}條資訊") }); } if (i % 4 == 3) { msgList.Add(new LogMsg() { LogType = "error", Msg = Encoding.UTF8.GetBytes($"error第{i}條資訊") }); } } Console.WriteLine("生產者傳送100條日誌資訊"); //傳送日誌資訊 foreach (var item in msgList) { channel.BasicPublish(exchange: "myexchange", routingKey: item.LogType, basicProperties: null, body: item.Msg); } } } Console.ReadKey(); } }
消費者C1用於處理log_error佇列中的訊息,錯誤訊息進行簡訊通知,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myexchange", type: ExchangeType.Direct, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "log_all", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結 channel.QueueBind(queue: "log_error", exchange: "myexchange", routingKey: "error"); //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); //只是為了演示,並沒有存入文字檔案 Console.WriteLine($"接收成功!【{message}】,傳送簡訊通知"); }; Console.WriteLine("消費者C1【接收錯誤日誌,傳送簡訊通知】準備就緒...."); //處理訊息 channel.BasicConsume(queue: "log_error", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
消費者C2用於處理log_all佇列中的訊息,所有訊息記錄到文字檔案中,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myexchange", type: ExchangeType.Direct, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "log_all", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結 string[] logtypes = new string[] { "debug", "info", "warn", "error" }; foreach (string item in logtypes) { channel.QueueBind(queue: "log_all", exchange: "myexchange", routingKey: item); } //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); //只是為了演示,並沒有存入文字檔案 Console.WriteLine($"接收成功!【{message}】,存入文字檔案"); }; Console.WriteLine("消費者C2【接收所有日誌資訊,存入文字檔案】準備就緒...."); //處理訊息 channel.BasicConsume(queue: "log_all", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
執行這三個專案,執行結果如下:
2 fanout型別
1 fanout路由規則
fanout型別的exchange路由規則是最簡單的,交換機會把訊息廣播到與該Exchange繫結的所有queue中,即所有和該exchange繫結的佇列都會收到訊息。fanout型別exchange和佇列繫結時不需要指定routingKey,即使指定了routingKey也會被忽略掉。路由結構如下:
fanout型別交換機主要用於釋出/訂閱的一些場景,如使用者註冊了我們的網站後,我們通過簡訊和郵件兩種方式通知使用者
2 程式碼示例
這裡通過程式碼簡單演示將訊息同時使用簡訊和郵件兩種方式通知使用者的流程。首先宣告一個fanout型別的exchange,然後宣告兩個佇列 SMSqueue和EMAILqueue,這兩個佇列都和這個exchange繫結。消費者1處理EMAILqueue的訊息,通過郵件方式傳送通知;消費者2處理SMSqueue的訊息通過簡訊方式傳送通知。
生產者傳送資訊,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //第一步:建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myfanoutexchange", type: ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null); //宣告SMSqueue佇列,用於簡訊通知 channel.QueueDeclare(queue: "SMSqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //宣告佇列,Email佇列,用於郵件通知 channel.QueueDeclare(queue: "EMAILqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結exchange和queue channel.QueueBind(queue: "SMSqueue", exchange: "myfanoutexchange", routingKey: string.Empty,arguments:null); channel.QueueBind(queue: "EMAILqueue", exchange: "myfanoutexchange", routingKey: string.Empty, arguments: null); Console.WriteLine("生產者準備就緒...."); string message = ""; //第六步:傳送訊息 //在控制檯輸入訊息,按enter鍵傳送訊息 while (!message.Equals("quit", StringComparison.CurrentCultureIgnoreCase)) { message = Console.ReadLine(); var body = Encoding.UTF8.GetBytes(message); //基本釋出 channel.BasicPublish(exchange: "myfanoutexchange", routingKey: string.Empty, basicProperties: null, body: body); Console.WriteLine($"訊息【{message}】已傳送到佇列"); } } } Console.ReadKey(); }
消費者1將EMAILqueue的訊息通過郵件方式傳送通知,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myfanoutexchange", type: ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "EMAILqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結exchange和queue channel.QueueBind(queue: "EMAILqueue", exchange: "myfanoutexchange", routingKey: string.Empty, arguments: null); //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); //只是為了演示,並沒有存入文字檔案 Console.WriteLine($"接收成功!【{message}】,郵件通知"); }; Console.WriteLine("郵件通知服務準備就緒..."); //處理訊息 channel.BasicConsume(queue: "EMAILqueue", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
消費者2將SMSqueue的訊息通過簡訊方式傳送通知,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myfanoutexchange", type: ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "SMSqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //繫結exchange和queue channel.QueueBind(queue: "SMSqueue", exchange: "myfanoutexchange", routingKey: string.Empty,arguments:null); //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); //只是為了演示,並沒有存入文字檔案 Console.WriteLine($"接收成功!【{message}】,簡訊通知"); }; Console.WriteLine("簡訊通知服務準備就緒..."); //處理訊息 channel.BasicConsume(queue: "myfanoutqueue", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
啟動這三個應用程式,執行結果如下:
3 topic型別
1 topic路由規則
topic型別exchange的路由規則也是基於routingKey和bindingKey的,其路由過程和direct型別基本一致,兩者的區別在於direct型別的exchange要求routingKey和bindingKey必須相同才進行將訊息路由到繫結的queue中,而topic型別的bindingKey是一個匹配規則,只要routingKey符合bindingKey的規則就可以將訊息路由到繫結的queue中去,結構如下圖所示。注意routingKey和bindingKey的結構都是一系列由點號連線單詞的字串,例如【aaa.bbb.ccc】。
bindingKey的兩個特殊符號:*表示一個單詞,#表示0或多個單詞(注意是單詞,而不是字元)。如下圖,usa.news和usa.weather都和usa.#匹配,而usa.news和europe.news都和#.news匹配。
2 程式碼實現
這裡使用程式碼實現上圖中的例子,為了方便我們只定義兩個佇列:接收美國相關資訊的usaQueue(bindingKey是usa.#)和接收新聞訊息的newsQueue(bindingKey是#.news)。然後定義兩個消費者,消費者1處理useaQueue的訊息,消費者2處理newsQueue的訊息。
生產者程式碼:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "mytopicExchange", type: ExchangeType.Topic, durable: true, autoDelete: false, arguments: null); //宣告佇列usaQueue channel.QueueDeclare(queue: "usaQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //宣告佇列newsQueue channel.QueueDeclare(queue: "newsQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); Console.WriteLine("生產者準備就緒...."); //繫結usaQueue佇列到互動機,routingKey為usa.# channel.QueueBind(queue: "usaQueue", exchange: "mytopicExchange", routingKey: "usa.#", arguments: null); //繫結newsQueue佇列到互動機,routingKey為#.news channel.QueueBind(queue: "newsQueue", exchange: "mytopicExchange", routingKey: "#.news", arguments: null); ////--------------------開始傳送訊息 //1.傳送美國新聞訊息 string message1 = "美國新聞訊息:內容balabala"; var body1 = Encoding.UTF8.GetBytes(message1); channel.BasicPublish(exchange: "mytopicExchange", routingKey: "usa.news", basicProperties: null, body: body1); Console.WriteLine($"訊息【{message1}】已傳送到佇列"); //2.傳送美國天氣訊息 string message2 = "美國天氣訊息:內容balabala"; var body2 = Encoding.UTF8.GetBytes(message2); channel.BasicPublish(exchange: "mytopicExchange", routingKey: "usa.weather", basicProperties: null, body: body2); Console.WriteLine($"訊息【{message2}】已傳送到佇列"); //3.傳送歐洲新聞訊息 string message3 = "歐洲新聞訊息:內容balabala"; var body3 = Encoding.UTF8.GetBytes(message3); channel.BasicPublish(exchange: "mytopicExchange", routingKey: "europe.news", basicProperties: null, body: body3); Console.WriteLine($"訊息【{message3}】已傳送到佇列"); //4.傳送歐洲天氣訊息 string message4 = "歐洲天氣訊息:內容balabala"; var body4 = Encoding.UTF8.GetBytes(message4); //基本釋出 channel.BasicPublish(exchange: "mytopicExchange", routingKey: "europe.weather", basicProperties: null, body: body4); Console.WriteLine($"訊息【{message4}】已傳送到佇列"); } } Console.ReadKey(); }
消費者1程式碼,只處理usaQueue中的訊息:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "mytopicExchange", type: ExchangeType.Topic, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "usaQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); Console.WriteLine("usaQueue消費者準備就緒...."); //繫結usaQueue佇列到互動機 channel.QueueBind(queue: "usaQueue", exchange: "mytopicExchange", routingKey: "usa.#", arguments: null); //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); Console.WriteLine($"接收成功!【{message}】"); }; //處理訊息 channel.BasicConsume(queue: "usaQueue", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
消費者2程式碼,只處理newsQueue中的訊息:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "mytopicExchange", type: ExchangeType.Topic, durable: true, autoDelete: false, arguments: null); //宣告佇列queue channel.QueueDeclare(queue: "newsQueue", durable: true, exclusive: false, autoDelete: false, arguments: null); Console.WriteLine("newsQueue消費者準備就緒...."); //繫結usaQueue佇列到互動機 channel.QueueBind(queue: "newsQueue", exchange: "mytopicExchange", routingKey: "#.news", arguments: null); //定義消費者 var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); Console.WriteLine($"接收成功!【{message}】"); }; //處理訊息 channel.BasicConsume(queue: "newsQueue", autoAck: true, consumer: consumer); Console.ReadLine(); } } }
生成者傳送的四條訊息中,訊息1的routingKey為usa.news,同時符合usaQueue的bindingKey(usa.#)和newsQueue的bindingKey(#.news),所以訊息1同時路由到兩個佇列中;訊息2的routingKey為usa.weather只符合usa.#,傳送到usaQueue;訊息的rouKey為europe.news,只符合#.news,傳送到newsQueue中;訊息4的routingKey為europe.weahter,和兩個佇列的bindingKey都不符合,所以被丟棄。執行這三個Console應用程式,結果如下:
一點補充:topic型別交換機十分靈活,可以輕鬆實現direct和fanout型別交換機的功能。如果繫結佇列時所有的bindingKey都是#,則交換機和fanout型別交換機表現一致;如果所有的bindingKey都不包含*和#,則交換機和direct型別交換機表現一致。
4 header型別
1 header路由規則
header型別路由規則和上邊的幾種exchange都不一樣,header型別exchange不是通過routingKey進行路由的,而是通過Headers。exchange在和queue進行binding時可以設定arguments:
channel.QueueBind(queue: "Allqueue", exchange: "myheaderExchange", routingKey: string.Empty, arguments: new Dictionary<string, object> { { "x-match","all"}, { "user","jack"}, { "pass","123"}
});
將訊息傳送到exchange時可以設定訊息的Header:
var props1 = channel.CreateBasicProperties(); props1.Headers = new Dictionary<string, object>() { { "user","jack"}, { "pass","123"} }; var body1 = Encoding.UTF8.GetBytes(msg1); //傳送訊息 channel.BasicPublish(exchange: "myheaderExchange", routingKey: string.Empty, basicProperties: props1, body: body1);
user和pass是普通的鍵值對,我們也可以設定其他的鍵值對。x-match是一個特殊的屬性,當x-match為all時,aguments和basicProrperties.Headers的所有鍵值對都相等時才會路由到queue(AND關係);當x-match為any時,aguments和basicProrperties.Headers的鍵值對只要有一個相同就可以路由到queue(OR關係)。
2.程式碼示例
看一個簡單的栗子,建立兩個佇列Allqueue和Anyqueue,其中Allqueue和exchange繫結時的x-match為all,Anyqueue和exchange繫結時的x-match為any;然後傳送兩條訊息,傳送第一條訊息時basicProperties.Headers中的user和pass都和繫結佇列時的agruments的user和pass相等,傳送第二條訊息是兩者的pass不相等,程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { //宣告交換機exchang channel.ExchangeDeclare(exchange: "myheaderExchange", type: ExchangeType.Headers, durable: true, autoDelete: false, arguments: null); //宣告Allqueue佇列 channel.QueueDeclare(queue: "Allqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); //宣告Anyqueue佇列 channel.QueueDeclare(queue: "Anyqueue", durable: true, exclusive: false, autoDelete: false, arguments: null); Console.WriteLine("生產者準備就緒...."); //////傳送訊息訊息1,user和pass都相同 //繫結exchange和Allqueue channel.QueueBind(queue: "Allqueue", exchange: "myheaderExchange", routingKey: string.Empty, arguments: new Dictionary<string, object> { { "x-match","all"}, { "user","jack"}, { "pass","123"}}); string msg1 = "user和pass都相同時傳送的訊息"; var props1 = channel.CreateBasicProperties(); props1.Headers = new Dictionary<string, object>() { { "user","jack"}, { "pass","123"} }; var body1 = Encoding.UTF8.GetBytes(msg1); //基本釋出 channel.BasicPublish(exchange: "myheaderExchange", routingKey: string.Empty, basicProperties: props1, body: body1); Console.WriteLine($"訊息【{msg1}】已傳送到佇列"); //////傳送訊息訊息2,user和pass不完全相同 //繫結exchange和Anyqueue channel.QueueBind(queue: "Anyqueue", exchange: "myheaderExchange", routingKey: string.Empty, arguments: new Dictionary<string, object> { { "x-match","any"}, { "user","jack"}, { "pass","123"},}); string msg2 = "user和pass不完全相同時傳送的訊息"; var props2 = channel.CreateBasicProperties(); props2.Headers = new Dictionary<string, object>() { { "user","jack"}, { "pass","456"}//這裡的pass和BindQueue方法的中argumens中的pass不相同 }; var body2 = Encoding.UTF8.GetBytes(msg2); //基本釋出 channel.BasicPublish(exchange: "myheaderExchange", routingKey: string.Empty, basicProperties: props2, body: body2); Console.WriteLine($"訊息【{msg2}】已傳送到佇列"); } } Console.ReadKey(); } }
執行程式,開啟WebUI管理介面,結果如下,我們看到只有user和pass都相等時訊息才會路由到Allqueue;user和pass只要有一個相等就會路由到Anyqueue
5 小結
RabbitMQ的交換機(exchange)的作用是路由訊息,我們可以根據應用場景的不同選擇合適的交換機。如果需要精準路由到佇列,或者對訊息進行單一維度分類(只對日誌的嚴重程度這一維度進行分類)可以使用direct型別交換機;如果需要廣播訊息,可以使用fanout型別的交換機;如果對訊息進行多維度分類(如例子中按照地區和訊息內容型別兩個維度進行分類)使用topic型別的交換機;如果訊息歸類的邏輯包含了較多的AND/OR邏輯判斷可以使用header型別交換機(開發中很少用到Header型別,官網上關於Header型別的介紹也不多)。
【參考文章】
1. https://www.cnblogs.com/zhangweizhong/p/5713874.html
2.https://blog.csdn.net/ctwy291314/article/details/83147194