在前面的教程中,我们对日志系统进行了功能强化。我们使用direct类型的交换器并且为之提供了可以选择接收日志的能力,替换了只能傻乎乎的广播消息的fanout类型的交换器。尽管使用direct类型的交换器强化了系统,但是它依然有一些限制,不能基于条件的进行路由。

在日志系统中,我们或许希望不仅能根据严重等级,还能基于日志的发送源来订阅日志日志。你可能已经从Unixsyslog工具中知道了这个概念,该工具路由日志的时候既基于严重等级(info/warn/crit...)又基于设备(auth/cron/kern...)。这将带来极大的灵活性,我们可能仅仅希望监听来自‘cron’的极其重要的错误,同时监听来自‘kern’的所有日志。为了在日志系统中实现它,我们需要学习更复杂的topic交换器。

Topic交换器

发送消息到topic交换器不能随心所欲的指定路由关键字(routing key),它必须是一个由点(.)分割的单词列表。这些单词可以是任意的,但是通常会在其中指定一些发送消息的特性,如“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”等便是一些合法的路由关键字。你可以在路由关键字中指定任意数量的单词,但是不能超过上限255字节。

绑定关键字必须是同样的样式,topic交换器的内部逻辑和direct交换器的内部逻辑很相似。使用特定路由关键字发送的消息会被转发到匹配绑定关键字的所有队列。关于绑定关键字,有两点需要特别注意:

  • *(星号)可以代表一个单词
  • #(井号)可以代表零个或多个单词

下图示例是对此最简单的说明:

                                         

在这个示例中,我们打算发送的全是用来描述动物的消息,这些消息会使用由三个单词(两个点)组成的路由关键字来发送。在路由关键字中,第一个单词描述速度,第二个单词描述颜色,第三个单词描述物种。即:

"<speed>.<colour>.<species>"。

我们将创建三个绑定,队列Q1绑定到关键字“*.orange.*”,队列Q2绑定到关键字“*.*.rabbit”和lazy.#”。这些绑定可以被总结如下:

  1. 队列Q1对所有橙色(orange)的动物感兴趣
  2. 队列Q2对兔子(rabbit)的所有事情和所有懒惰(lazy)动物的事情感兴趣

一个被设置路由关键字被设置为“quick.orange.rabbit”的消息将会被同时发布到两个队列,路由关键字为“lazy.orange.elephant”的消息也会被同时发布到两个队列。反之,路由关键字为“quick.orange.fox”的消息将会被发布到队列Q1,路由关键字为“lazy.brown.fox”的消息将被发布到队列Q2。而路由关键字为“lazy.pink.rabbit”的消息只会被发布到队列Q2一次,尽管它匹配两个绑定。路由关键字为“quick.brown.fox”的消息不匹配任意一个队列,故该消息会被销毁。

如果我们打破约定,使用一个单词或者四个单词作为路由关键字(例如:“orange”或“quick.orange.male.rabbit”)会发生什么呢?这类消息因为不匹配任意一个绑定,所以将会丢失。相反的,“lazy.orange.male.rabbit”虽然有四个单词,但是它匹配最后一个绑定,消息将会被发布到队列Q2中。

 

Topic交换器

Topic交换器非常强大,能表现出其他交换器的行为。当一个队列用井号(#)作为绑定,它将接收所有的消息,忽略路由关键字,像fanout交换器一样。当在绑定中不使用特殊字符星号(*)和井号(#)时,它看起来就像direct交换器了。

 

组合在一起

我们将要在日志系统中使用topic交换器,从假设日志的路由关键字由两个单词组成(<facility>.<severity>|<设备>.<严重程度>)。

代码和上一个教程中是一样的,EmitLogTopic.cs中的代码:

 1 using System;
 2 using System.Linq;
 3 using RabbitMQ.Client;
 4 using System.Text;
 5 
 6 class EmitLogTopic
 7 {
 8     public static void Main(string[] args)
 9     {
10         var factory = new ConnectionFactory() { HostName = "localhost" };
11         using(var connection = factory.CreateConnection())
12         using(var channel = connection.CreateModel())
13         {
14             channel.ExchangeDeclare(exchange: "topic_logs",
15                                     type: "topic");
16 
17             var routingKey = (args.Length > 0) ? args[0] : "anonymous.info";
18             var message = (args.Length > 1)
19                           ? string.Join(" ", args.Skip( 1 ).ToArray())
20                           : "Hello World!";
21             var body = Encoding.UTF8.GetBytes(message);
22             channel.BasicPublish(exchange: "topic_logs",
23                                  routingKey: routingKey,
24                                  basicProperties: null,
25                                  body: body);
26             Console.WriteLine(" [x] Sent '{0}':'{1}'", routingKey, message);
27         }
28     }
29 }

ReceiveLogsTopics.cs中的代码:

 1 using System;
 2 using RabbitMQ.Client;
 3 using RabbitMQ.Client.Events;
 4 using System.Text;
 5 
 6 class ReceiveLogsTopic
 7 {
 8     public static void Main(string[] args)
 9     {
10         var factory = new ConnectionFactory() { HostName = "localhost" };
11         using(var connection = factory.CreateConnection())
12         using(var channel = connection.CreateModel())
13         {
14             channel.ExchangeDeclare(exchange: "topic_logs", type: "topic");
15             var queueName = channel.QueueDeclare().QueueName;
16 
17             if(args.Length < 1)
18             {
19                 Console.Error.WriteLine("Usage: {0} [binding_key...]",
20                                         Environment.GetCommandLineArgs()[0]);
21                 Console.WriteLine(" Press [enter] to exit.");
22                 Console.ReadLine();
23                 Environment.ExitCode = 1;
24                 return;
25             }
26 
27             foreach(var bindingKey in args)
28             {
29                 channel.QueueBind(queue: queueName,
30                                   exchange: "topic_logs",
31                                   routingKey: bindingKey);
32             }
33 
34             Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C");
35 
36             var consumer = new EventingBasicConsumer(channel);
37             consumer.Received += (model, ea) =>
38             {
39                 var body = ea.Body;
40                 var message = Encoding.UTF8.GetString(body);
41                 var routingKey = ea.RoutingKey;
42                 Console.WriteLine(" [x] Received '{0}':'{1}'",
43                                   routingKey,
44                                   message);
45             };
46             channel.BasicConsume(queue: queueName,
47                                  noAck: true,
48                                  consumer: consumer);
49 
50             Console.WriteLine(" Press [enter] to exit.");
51             Console.ReadLine();
52         }
53     }
54 }

运行下面的示例:

接收所有的日志:

1 $ ReceiveLogsTopic.exe "#"

接收来自设备“kern”的所有日志:

1 $ ReceiveLogsTopic.exe "kern.*"

或者你只想关注“严重(critical)”等级的日志:

1 $ ReceiveLogsTopic.exe "*.critical"

你也可以创建多个绑定:

1 $ ReceiveLogsTopic.exe "kern.*" "*.critical"

使用路由关键字“kern.critical”发送一条日志:

1 $ EmitLogTopic.exe "kern.critical" "A critical kernel error"

希望这些程序能让你玩的开心。注意,这些代码并没有对路由或绑定关键字做任何假设,你也可以尝试超过两个参数的路由关键字。

下面是一些棘手的问题:

  1. 使用星号(*)的绑定是否能捕获到空路由关键字的消息?
  2. 使用“#.*”的绑定是否会将“..”作为一个关键字?是否会捕获仅使用单个单词作为关键字的消息?
  3. a.*.#”和“a.#”有什么不同?

下一步,在教程六种将了解如何在远程过程调用中使用往返消息。

 

原文链接:http://www.rabbitmq.com/tutorials/tutorial-five-dotnet.html

posted on 2015-11-15 09:17  hptony  阅读(1318)  评论(2编辑  收藏  举报