主题
Topics(using the java client)
在前面的章节中,我们改进了日志系统。没有使用只能进行虚拟广播的
fanout
交换机,而是使用了direct
交换机,从而获得了选择性接收日志的可能性。
尽管使用direct
交换机改进了我们的系统,但它仍然有局限性——它不能基于多个标准进行路由。
在日志系统中,可能不仅希望根据严重程度订阅日志,还希望根据发出日志的源订阅日志。你可能从syslog
unix工具中了解到这个概念,该工具可以根据严重程度(info/warn/crit…)和设备(auth/cron/kern…)来路由日志。
这提供了很大的灵活性——我们可能想要监听来自’cron’的严重错误,但也想要监听来自’kern’的所有日志。
要在我们的日志系统中实现它,需要了解一个更复杂的topic
交换机。
topic
交换机
发送到
topic
交换机的消息不能有任意的路由键——它必须是由点分隔的单词列表。这些词可以是任何词,但通常它们指明了与信息相关的一些特征。以下是一些有效的路由键示例:stock.usd.nyse
、nyse.vmw
、quick.orange.rabbit
。路由键中可以有任意多的单词,最多为255个字节。
绑定键的形式也必须相同。topic
交换机背后的逻辑与direct
交换机类似——带有特定路由键的消息将被发送到所有绑定了匹配绑定键的队列。然而,绑定键有两种重要的特殊情况:
*
可以替换1个单词。#
可以替换0个或多个单词。
在示例中解释这一点是最简单的:
在这个例子中,将发送描述动物的信息。这些消息将与由三个单词(两个点)组成的路由关键字一起发送。路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种:<speed>.<colour>.<species>
。
我们创建了三个绑定: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
即使它有四个单词,也和最后一个绑定相匹配,将会被发送到第二个队列。
topic
交换机功能很强大,可以表现得像其他交换机一样工作。
当队列被绑定到#
绑定键时,它将接收所有消息,而不管路由键是什么——就像fanout
交换机一样。而当特殊字符*
和#
不在绑定中使用时,topic
交换机的行为就像是direct
交换机一样。
整合代码
我们将在日志系统中使用
topic
交换机。首先假设日志的路由键有两个单词:<facility>.<severity>
。
EmitLogTopic.java
类的代码:
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
var factory = new ConnectionFactory();
factory.setHost("192.168.1.254");
factory.setUsername("admin");
factory.setPassword("admin123");
try (var connection = factory.newConnection();
var channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
var routingKey = getRouting(argv);
var message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, routingKey, null,
message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
}
private static String getRouting(String[] strings) {
if (strings.length < 1) {
return "anonymous.info";
}
return strings[0];
}
private static String getMessage(String[] strings) {
if (strings.length < 2) {
return "Hello World!";
}
return joinStrings(strings, " ", 1);
}
private static String joinStrings(String[] strings, String delimiter, int startIndex) {
int length = strings.length;
if (length == 0) {
return "";
}
if (length < startIndex) {
return "";
}
var words = new StringBuilder(strings[startIndex]);
for (int i = startIndex + 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
ReceiveLogsTopic.java
类的代码:
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
var factory = new ConnectionFactory();
factory.setHost("192.168.1.254");
factory.setUsername("admin");
factory.setPassword("admin123");
var connection = factory.newConnection();
var channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
var queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
System.exit(1);
}
for (var 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(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey()
+ "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
}
}