在上一个教程中,我们改进了日志记录系统。我们没有使用只能进行无脑广播的扇形交换机,而是使用了直连交换机,并获得了选择性接收日志的可能性。
虽然使用直接交换改进了我们的系统,但它仍然有局限性-它不能基于多个标准进行路由。
在我们的日志系统中,我们可能不仅要订阅基于严重性的日志,还需要基于发出日志的源。您可能从 syslog unix tool中知道这个概念,该工具基于优先级(info/warn/crit…)和设备(auth/cron/kern…)路由日志。
这将给我们很大的灵活性-我们可能只想监听来自“cron”的错误日志,也可以监听“kern”的所有日志。
为了在我们的日志系统中实现这一点,我们需要了解一个更复杂的主题交换机。
主题交换机
发送到主题交换机的消息不能有任意的路由键-它必须是由点分隔的单词列表。单词可以是任何内容,但通常它们指定与消息相关的一些特性。一些有效的路由键示例:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”. 路由键中可以有任意多个字,最多不超过255个字节。
绑定键的格式也必须相同。主题交换背后的逻辑类似于直接交换——使用特定路由键发送的消息将被传递到使用匹配绑定键绑定的所有队列。但是,绑定密钥有两种重要的特殊情况:
*可以代替一个词。
#可以代替零个或多个单词。
用一个例子来解释这一点最简单:
在这个例子中,我们将发送所有描述动物的信息。消息将带着路由键发送,路由键由三个单词(两个点)组成。路由键中的第一个词将描述速度,第二个词是颜色,第三个词是“物种”:“”。
我们创建了三个绑定:Q1用绑定键“.orange.”绑定,Q2用“..rabbit”和“lazy.#。
这些绑定可以概括为:
- Q1对所有橙色动物都感兴趣。
- Q2想听关于兔子的一切,以及关于懒惰动物的一切。
路由键设置为“"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",即使它有四个单词,也将匹配最后一个绑定并将被传递到第二个队列。
主题交换机
主题交换机功能强大,可以表现的像其他交换机一样。
当队列与“#”(哈希)绑定key绑定时,它将接收所有消息,而不管路由密钥是什么,就像扇形交换机一样。
当绑定中不使用特殊字符“*”(星)和“#”(散列)时,主题交换将像直接交换一样。
把它们放在一起
我们将在日志系统中使用主题交换。我们首先假设日志的路由键将有两个词:“”。
代码与上一个教程中的代码几乎相同。
代码EmitLogTopic.java:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_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, "topic");
String routingKey = getRouting(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
}
//..
}
代码 ReceiveLogsTopic.java:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
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("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "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");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
编译并运行示例,包括教程1中的类路径-在Windows上,使用%CP%。
编译:
javac -cp $CP ReceiveLogsTopic.java EmitLogTopic.java
要接收所有日志:
java -cp $CP ReceiveLogsTopic "#"
要接收从设备“kern”来的所有日志:
java -cp $CP ReceiveLogsTopic "kern.*"
或者,如果您只想了解“严重”日志:
java -cp $CP ReceiveLogsTopic "*.critical"
可以创建多个绑定:
java-cp$cp ReceiveLogsTopic“kern.*”*.critical
并发出带有路由键 “kern.critical” 类型的日志消息:
java -cp $CP EmitLogTopic "kern.critical" "A critical kernel error"
祝你和这些程序玩得开心。请注意,代码没有对路由或绑定键进行任何假设(限制),你可以使用两个以上的路由密钥键。
接下来,在教程6中了解如何将往返消息作为远程过程调用