发布/订阅
在上一个教程中我们创建了一个工作队列。如果说工作队列是将一个任务完全分发给一个消费者。在这部分,我们所做的完全不同 —— 我们将把一个消息交付给多个消费者。这种模式称之为发布/订阅(publish/subscribe
)。
为了说明这种模式,我们创建一个简单的日志系统。它由两个程序组成 —— 第一个(生产者)将发出一个日志消息,第二个(消费者)将接收它并打印它。
在我们的日志系统中每个接收者程序都能得到消息的拷贝。这样我们就可以跑一个接收者并直接将日志记录到磁盘;与此同时能跑另一个接收者将日志打印在屏幕。
本质上,发布一个日志消息将被广播到所有的接收者。
交换机(exchange)
在前面的教程中,我们向队列发送消息,从队列中接收消息。现在是时候介绍Rabbit中完整的消息传递模型了。
让我们快速的回顾下之前的教程:
- 生产者是一个用来发送消息的用户应用
- 队列用来缓存存储消息
- 消费者是一个用来接收消息的用户应用
RabbitMQ的消息模型的核心思想是生产者不会直接的向队列发送任何消息。实际上,生产者都不知道将消息交付给哪个队列。
相反的,生产者只将消息往exchange中发送,exchange是非常简单的。一边从生产者中接收消息,一边将消息推到队列。exchange能正确的知道如何处理接收到的消息。是添加到一个特定的队列?是添加到很多队列?还是会丢弃。这些规则都通过exchange类型来定义。
exchange有以下几种类型:
- direct
- topic
- headers
- fanout
我们目前关注最后一个 —— fanout
。让我们新建一个这样的exchange类型,并命名为logs
。
channel.exchangeDeclare("logs", "fanout");
fanout exchange
非常简单。你可以尽可能的从它的名字中猜测,它只是将接收到的消息广播到它所知道的队列。这正是我们需要的日志。
exchange 列表
你想在服务中查看exchange的列表,你能使用命令
rabbitmqctl
:
sudo rabbitmqctl list_exchanges
在列表中有些默认的、格式为amq.*的exchange。这些都是默认创建的,但是你不太可能需要使用到它们。
匿名 exchange
在前面的教程中我们对exchange一无所知,但是我们还是能将消息发送到队列。之所以可以,因为我们使用了默认的exchange,就是我们用的空字符串("")。
回想我们之前发布的一个消息:
channel.basicPublish("", "hello", null, message.getBytes());
第一个参数就是exchange的名称。空字符表示默认的或者匿名的exchange。消息路由到指定routingKey的队列中,如果它存在。
现在,我们能发布我们命名的exchange了:
channel.basicPublish( "logs", "", null, message.getBytes());
临时队列
还记得前面教程中我们使用的队列都是指定名字的(hello、work.queue)。命名一个队列对我们来是至关重要的——我们需要指定工作者到相同的队列中。当你想在生产者和消费者之间共享队列时,给队列命名是很重要的。
但是在我们的日志中者不是我们要关心的。我们想得到所有的日志消息,而不是他们的子集。我们感兴趣的也只是当前活动的消息而不是老的那个。为了解决这个我们需要做两个步骤:
第一,无论何时我们连接Rabbit需要一个新的,空的队列。要达到这个我们需要在创建队列的时候给它随机一个名字,或者更好方案 —— 让服务选择一个随机队列名给我们。
第二,一旦我们消费完队列断开连接就自动的删除队列。
我们提供了一个无参方法queueDeclare()
创建一个非持久化、独有的、自动删除的、随机生成名字的队列。
String queueName = channel.queueDeclare().getQueue();
队列名是随机的,它的格式类似于:amq.gen-JzTY20BRgKO-HjmUJj0wLg
。
绑定(bindings)
我们已经创建了一个fanout exchange和一个队列。现在我们需要告诉exchange发送消息到我们的队列。这种exchange和队列的关联关系称之为绑定binding
。
channel.queueBind(queueName, "logs", "");
现在在logs exchange中可以将消息附加到我们的队列
绑定列表
你能显示当前正在使用的bindings列表
rabbitmqctl list_bindings
信息汇总
这个发送日志消息的生产者程序,和之前的教程没有相差很多。最重要的改变是我们用名称为logs
的exchange代替了匿名的写法。当我们发送消息的时候要提供一个routingKey,但是在类型为fanout
的exchange中,我们忽略它。下面是EmitLog.java整个程序的代码。
package com.roachfu.tutorial.rabbitmq.website.fanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义 exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = "this is fanout exchange";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
如你所见,建立连接之后我们定义了exchange。这步是必要的,推送到一个不存在的exchange是不予许的。
如果没有队列绑定到exchange,消息将被丢失,但是对我们来说是可以的;如果没有消费者监听消息,我们能安全的丢弃消息。
官网使用命令行实现的。能很好的实现是显示在控制台还是记录到日志文件中。这里我们写两个程序,一个用来在console控制台打印消息,一个用来将消息记录到日志文件中。
ReceiveLogsToConsole.java
package com.roachfu.tutorial.rabbitmq.website.fanout;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 打印消息到控制台
*/
public class ReceiveLogToConsole {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义exchange,消费者和生产者都要定义。因为并不知道exchange存不存在。
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 获取队列名
String queryName = channel.queueDeclare().getQueue();
// 将队列名和exchange进行绑定
channel.queueBind(queryName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for message and handle it to console . . . ");
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 '" + message + "'");
}
};
channel.basicConsume(queryName, true, consumer);
}
}
ReceiveLogToFile.java
package com.roachfu.tutorial.rabbitmq.website.fanout;
import com.rabbitmq.client.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 接收消息并打印到文件
*/
public class ReceiveLogToFile {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
Connection connection = connectionFactory.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 message and handle it to file. . . ");
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");
File file = new File("/Users/temp/fanout.log");
FileOutputStream out = new FileOutputStream(file, true);
out.write(body);
out.write(("\r\n").getBytes());
out.flush();
out.close();
}
};
channel.basicConsume(queueName, true, consumer);
}
}
测试结果
我们先启动两个消费者,然后再启动三次生产者。
ReceiveLogToConsole.java 控制台显示:
[*] Waiting for message and handle it to console . . .
[x] Received message 'this is fanout exchange'
[x] Received message 'this is fanout exchange'
[x] Received message 'this is fanout exchange'
ReceiveLogToFile.java 文件显示