在上一篇教程中,我们改进了日志系统。我们使用的是 fanout 类型的交换器,而不是使用一个只能进行虚拟广播的 fanout 交换器,并且获得了有选择性地接收日志的功能。
尽管使用 direct 交换器改进了我们的系统,但是它仍然有局限性 —— 它不能基于多个标准进行路由。
在我们的日志系统中,我们可能不仅要根据严重程度订阅日志,而且还要基于发出日志的源头。你可能从syslog unix工具中知道这个概念,它根据严重性(info/war/crit.)和设施(auth/cr/kern.)来路由日志。
这将给我们带来很大的灵活性——我们可能想要侦听来自“cron”的关键错误,但也会侦听来自“kern”的所有日志。
为了在我们的日志系统中实现这一点,我们需要了解一个更复杂的 Topic 交换器。
1、Topic Exchange
发送到主题交换器的消息不能有任意的路由键 —— 它必须是一个由点分隔的单词列表。这些单词可以是任何东西,但通常它们指定与消息相连接的一些特性。一些有效的路由关键示例:stock.usd.nyse
、nyse.vmw
、quick.orange.rabbit
。在路由键中可以有任意多的单词,最多可以达到255字节的限制。
绑定键也必须以相同的形式出现。Topic
交换器背后的逻辑与 direct
交换器的逻辑相似 —— 用特定的路由键发送的消息将被传递给所有与匹配绑定键绑定的队列。然而,有两个重要的绑定键的特殊情况:
- *(star)可以代替一个词。
- #(hash)可以代替零个或多个单词。
现在用一个容易理解的例子来解释:
在这个例子中,我们将发送所有描述动物的消息。这些消息将使用一个由三个单词(两个点)组成的路由键发送。路由键的第一个词将描述速度,第二种颜色和第三种:速度。格式为:”..”.
我们创建了三个绑定:Q1绑定到绑定键为*.orange.*
,然后 Q2 绑定到 *.*.rabbit
和 “lazy.#” 上。
这些绑定可以概括为:
- Q1对所有的 orange 动物都很感兴趣。
- Q2 想要听到关于 rabbit 的一切,以及 lazy 的动物。
路由键被设置为 quick.orange.rabbit
的消息将被传送到两个队列中。消息 lazy.orange.elephant
也会去找它们。另一方面,quick.orange.fox
只会去第一个队列,`lazy.brown.fox`只到第二个队列。
lazy.pink.rabbit只会被传送到第二个队列消费一次,即使它与两个绑定相匹配。
quick.brown.fox“ 不匹配任何绑定,所以它会被丢弃。
如果我们违反了约定,用一个或四个词来发送信息,比如orange
或quick.orange.male.rabbit
,会发生什么呢?好吧,这些消息不会匹配任何绑定,将会丢失。
另一方面,lazy.orange.male.rabbit
,即使它有四个单词,它将匹配最后一个绑定,并将被传送到第二个队列。
主题交换器
主题交换功能非常强大,它可以像其他交换器一样运作。
当队列与#
(hash)绑定键绑定时,它将不考虑路由键接收所有的消息 ——就好像 fanout 交换器。
当在绑定中不使用特殊字符*
(star)和”#” (hash)时,主题交换将表现得像一个直接的字符。
2、示例
我们将在我们的日志系统中使用 Topic 交换器。我们将从一个工作假设开始,即日志的路由键将有两个单词:<facility>.<severity>
。
代码几乎与上一教程中的代码相同。
EmitLogTopic.java
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) {
Connection connection = null;
Channel channel = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
connection = factory.newConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String routingKey = "kern.critical";
String message = "A critical kernel error";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (connection != null) {
try {
connection.close();
}
catch (Exception ignore) {}
}
}
}
}
下面是 ReceiveLogsTopic.java:
的代码:
ReceiveLogsTopic.java
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
System.exit(1);
}
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
在 Idea 使用 Application 模式带参数启动 ReceiveLogsTopic.java 程序,比如带程序参数为 #
启动。
然后使用 debug 启动的时候就可以看到 main 方法传入的值为 #
。
- 接收所有的日志,使用带
#
参数启动 ReceiveLogsTopic.java 程序 - 从设备
kern
接收所有日志:使用带kern.*
参数启动 ReceiveLogsTopic.java 程序 - 如果你只想听到
critical
日志:使用带*.critical
参数启动 ReceiveLogsTopic.java 程序 - 创建多个绑定:使用带
kern.*
和*.critical
参数启动 ReceiveLogsTopic.java 程序
分别启动上面 4 个带参数的 Topic 交换器类型的 consumer。然后再启动生产消息 provider – EmitLogTopic.java。可以看到上面 4 个 consumer 都会收到 provider 发送的消息,并打印到控制台。