RabbitMQ指南(中)
在上一篇文章中,介绍了使用RabbitMQ的Hello World例子, 以及如何创建一个work queue。在work queue的例子中每条消息都只会被传递到一个work queue中。 在这篇文章中我们将会学习另一种完全不同的传递消息的方式——每条消息将会被传递给所有的consumer,这种模式一般被称为“发布/订阅”。
发布/订阅(Publish/Subscribe)
为了说明这种模式,我们将创建一个简单的log系统,它由两部分组成——第一部分负责发送log消息,第二部分负责接收并且将消息打印出来。 在我们的log系统中每个运行着的接收程序都会接收到消息,在这种方式下我们可以有一个consumer负责将log持久化到磁盘, 同时由另一个consumer来将log打印到控制台。本质上,发送log消息是对所有消息接收者的广播。
Exchange
在之前的部分我们都是通过queue来发送和接收消息,现在是时候来介绍RabbitMQ完整的消息模型了。先让我们来快速地回顾一下之前介绍过的几个概念:
- producer是用户应用负责发送消息
- queue是存储消息的缓冲(buffer)
- consumer是用户应用负责接收消息
RabbitMQ的消息模型的核心思想是producer永远不会直接发送任何消息到queue中,实际上,在很多情况下producer根本不知道一条消息是否被发送到了哪个queue中。
在RabbitMQ中,producer只能将消息发送到一个exchange中。要理解exchange也非常简单,它一边负责接收producer发送的消息, 另一边将消息推送到queue中。exchange必须清楚的知道在收到消息之后该如何进行下一步的处理,比如是否应该将这条消息发送到某个queue中? 还是应该发送到多个queue中?还是应该直接丢弃这条消息等等。用官方文档上的一张图可以更清楚地了解RabbitMQ的消息模型。
RabbitMQ中的exchange类型有这么几种:direct,topic,headers以及fanout。这一小节将会主要介绍最后一种类型——fanout。 使用RabbitMQ的client来创建一个fanout类型的exchange,命令为logs:
channel.exchangeDeclare("logs","fanout");
fanout类型的exchange非常简单,从名字也可以猜测出来,它会向所有的queue广播所有收到的消息。这正是我们的log系统需要的。
在之前的部分我们对exchange一无所知,但是我们仍然可以将消息发送到queue中,这是因为我们使用了默认的exchange,在代码中使用空字符串(“”)表示。
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数表示exchange的名字,使用空字符串表示使用默认的无名的exchange:如果有的话,消息将根据routingKey被发送到指定的queue中。
现在,可以将消息发送到之前已经声明过的exchange中
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
在之前的小节中使用queue都是指定了名字的(hello和task_queue),给queue命名是非常重要的,因为我们需要将的workers指定到相同的queue上, 并且在consumer与producer之间也需要指定相同的queue。
但是这对我们的log系统来说不是必须的,我们需要监听所有的log消息,而不是其中的一部分。我们也只关心现在的消息而不关注以前的消息, 为了解决这个问题我们需要做两件事情。
首先,无论何时连接到RabbitMQ server上都需要一个新的、空的queue。为了做到这一点需要能够使用一个随机的名字来创建queue, 更好的方式是由server来为我们选择一个随机的名字。
其次,一旦我们与consumer断开连接,queue应该被自动删除。
在Java client中,提供了一个无参数的queueDeclare()方来来创建一个非持久化的、独有的并且是自动删除的已命名的queue。
1
String queueName = channel.queueDeclare().getQueue();
queueName会包含一个随机的queue名字,可能看起来类似amq.gen-JzTY20BRgKO-HjmUJj0wLg。
绑定
我们已经创建了一个fanout类型的exchange和一个queue。现在我们需要告诉exchange将消息发送到我们的queue中。 这种exchange和queue的关系称为绑定(binding)。
channel.queueBind(queueName, "logs", "");
之后logs exchange将会把消息发送到我们的queue中。
完整的EmitLog.java代码
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv)
throws java.io.IOException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
//...
}
可以看到,在创建连接之后声明exchange。这一步是必要的,因为将消息发送到一个不存在的exchange是被禁止的。
如果还没有queue被绑定到exchange上,那么消息将会丢失,但这对我们来说是可以接收的,如果没有consumer正在监听消息, 那么可以安全的丢弃这些消息。
完整的ReceiveLogs.java代码
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "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, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
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 '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
Routing
在上一小节中我们构建了一个简单的log系统,可以向许多接收者广播消息。在这一小节中我们将会对此增加一个特性——可以只订阅消息的一部分。 举例来说,可以只将critical级别的错误日志持久化到磁盘,同时又能够将所有的消息打印到控制台。
绑定(binds)
在前一小节中已经介绍了如何创建绑定
channel.queueBind(queueName, EXCHANGE_NAME, "");
绑定是exchange和queue之间的一种关系,这可以简单的理解为:这个queue对这个exchange中的消息感兴趣。
绑定可以使用一个额外的routingKey参数,为了避免和basic_publish参数混淆,我们称它为binding key。 我们可以这样来使用key创建一个绑定:
channel.queueBind(queueName, EXCHANGE_NAME, "black");
binding key的含义取决于不同的exchange类型,我们之前使用的fanout类型会直接忽略这个值。
Direct exchange
我们之前的log消息系统将所有的消息广播到所有的consumer中。我们需要对此进行扩展,允许根据log的级别进行消息的过滤。 之前使用的fanout类型的exchange,没有提供给我们类似的灵活性——它只能简单的广播所有的消息。
在这里将会使用direct类型的exchange作为代替。direct类型的exchange的路由算法很简单——消息将会被传递到与它的routing key完全相同的 binding key的queue中。
还是使用一张图来说明:
还是使用一张图来说明:
在图中可以看到,有两个queue被绑定到了direct类型的exchange X上。第一个queue使用bing key orange绑定,第二个queue使用了两个binding key, 分别为black和green。
在这样的情况下,使用routing key为orange发送的消息将会被路由到queue Q1中,使用routing key为black或者green的将会被路由到Q2中。 所有其他的消息将会被丢弃。
多重绑定(Multiple bindings)
将多个queue使用相同的binding key进行绑定也是可行的。在我们的例子中可以在X和Q1中间增加一个binding key black。 在这种情况下,direct类型的exchange的行为将和fanout类似,它会向所有匹配的queue进行广播,使用routing key为black发送的消息将会同时被Q1和Q2接收。
发送log
我们将会为log系统使用这种模型。使用direct类型的exchange代替fanout。我们将会通过routing key提供log的严重级别。 使用这种方式可以选择不同的log严重级别来接收消息。首先来看发送log的部分。
创建一个exchange:
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
已经准备好发送消息:
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
为了简单起见,我们假设日志的级别只会为’info,’warning’,’error’三者中的一个。
订阅
接受消息部分将会和上一小节相同,除了一个例外——我们将会为每个感兴趣的严重级别创建新的绑定。
String queueName = channel.queueDeclare().getQueue();
for(String severity : argv){
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
完整的EmitLogDirect.java代码
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv)
throws java.io.IOException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String severity = getSeverity(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
channel.close();
connection.close();
}
//..
}
完整的ReceiveLogsDirect.java代码
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_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, "direct");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1){
System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
System.exit(1);
}
for(String severity : argv){
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
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);
}
}
可以在命令行中传入感兴趣的日志的严重级别来绑定。
喜欢小编的可以给小编点点关注~小编每天会不定时更新的