在本章中,将包含下面的内容:
- 连接中间件
- 生产消息
- 消费消息
- 使用JSON序列化消息
- 使用RPC消息
- 广播消息
- 使用direct交换器来处理消息路由
- 使用topic交换器来处理消息路由
- 保证消息处理
- 分发消息到多个消费者
- 使用消息属性
- 事务消息
- 处理未路由消息
要运行本章内的示例,你需要首先:
- 安装Java JDK 1.6+
- 安装Java RabbitMQ client library
- 正确地配置CLASSPATH 以及你喜欢的开发环境(Eclipse,NetBeans, 等等)
- 在某台机器上安装RabbitMQ server (也可以是同一个本地机器)
连接到中间件 每个使用AMQP的应用程序都必须建立一个与AMQP中间件的连接.默认情况下,RabbitMQ (以及任何其它1.0版本之前的AMQP中间件) 通过运行于5672端口之上且相当可靠传输协议-TCP来工作的, 即IANA分配的端口.
要创建一个连接RabbitMQ中间件的Java客户端,你必须执行下面的步骤:
1. 从Java RabbitMQ client library中必须的类:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
2. 创建客户端ConnectionFactory的实例:
ConnectionFactory factory = new ConnectionFactory();
3. 设置ConnectionFactory 选项:
factory.setHost(rabbitMQhostname);
4. 连接RabbitMQ broker:
Connection connection = factory.newConnection();
5. 从刚创建的连接中创建一个通道:
Channel channel = connection.createChannel();
6. 一旦在RabbitMQ上完成了工作,就需要释放通道和连接:
channel.close();
connection.close();
How it works…
使用Java client API, 应用程序必须创建一个ConnectionFactory实例,并且使用setHost()方法来设置运行RabbitMQ的主机.在导入相关类后(第1步),我们实例化了工厂对象(第2步).在这个例子中,我们只是用可选的命令行参数来设置主机名称,但是,在后面的章节中,你可以找到更多关于连接选项的信息.第4步,实际上我们已经创建了连接到RabbitMQ中间件的连接.
在这里,我们使用了默认的连接参数,用户:guest,密码:guest,以及虚拟主机:/,后面我们会讨论这些参数.
但现在我还没有准备好与中间件通信,我们必须设置一个通信的channel(第5步).这是AMQP中的一个高级概念,使用此抽象,可以让多个不同的消息会话使用同一个逻辑connection.
实际上, Java client library 中的所有通信操作都是通过channel实例的方法来执行的.如果你正在开发多线程应用程序,强烈建议在每个线程中使用不同的channel.如果多个线程使用同一个channel,在channel方法调用中会顺序执行,从而导致性能损失.最佳实践是打开一个connection,并将其在多个不同线程之间分享.每个线程负责其独立channel的创建,使用和销毁.
可对任何RabbitMQ connection指定多个不同的可选属性.你可以在在线文档(http://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc)上找到它们. 除了AMQP 虚拟主机外,其它选项都不需要说明.
虚拟主机是一个管理容器,在单个RabbitMQ实例中,允许配置多个逻辑上独立的中间件主机, 以让多个不同独立应用程序能够共享同一个RabbitMQ server. 每个虚拟主机都能独立地配置权限,交换器,队列,并在逻辑上独立的环境中工作.
也可以连接字符串(连接URI)来指定连接选项,即使用factory.setUri() 方法:
ConnectionFactory factory = new ConnectionFactory();
String uri="amqp://user:pass@hostname:port/vhost";
factory.setUri(uri);
URI必须与 RFC3 (http://www.ietf.org/rfc/rfc3986.txt)的语法规范保持一致.
生产消息
在本配方中, 我们将学习了如何将消息发送到AMQP队列. 我们将介绍AMQP消息的构建块:消息,队列,以及交换器.你可以在Chapter01/Recipe02/src/rmqexample中找到代码.
w to do it…
在连接到中间件后, 像前面配方中看到的一样,你可以按下面的步骤来来发送消息:
1. 声明队列, 在 com.rabbitmq.client.Channel上调用queueDeclare()方法:
String myQueue = "myFirstQueue";
channel.queueDeclare(myQueue, true, false, false, null); //创建一个名为myFirstQueue,持久化的,非限制的,不自动删除的队列,
2. 发送第一个消息到RabbitMQ broker:
String message = "My message to myFirstQueue";
channel.basicPublish("",myQueue, null, message.getBytes());
3. 使用不同的选项发送第二个消息:
channel.basicPublish("",myQueue,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
注意:队列名称是大小写敏感的: MYFIRSTQUEUE与myFirstQueue是不同的.
如何工作
在第一个基本例子中,我们能够发送一个消息到RabbitMQ.在信道建立后,第一个步骤可以确保目标队列存在,这项任务是通过调用queueDeclare()方法来声明队列的(步骤1).
如果队列已经存在的话,此方法不会做任何事情,否则,它会自己创建一个队列.如果队列已存在,但使用了不同的参数进行创建,queueDeclare() 方法会抛出异常.
注意,大部分的AMQP操作只是Channel Java接口的方法.
所有与broker交互的操作都需要通过channel来实施.
让我们来深入探讨queueDeclare() 方法. 其模板可以在http://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/查看. 文档看起来像下面这样:
实际上我们使用了第二个重载的方法:
AMQP.Queue.DeclareOk queueDeclare(java.lang.String queue, boolean durable, boolean exclusive, booleanautoDelete,java.util.Map<java.lang.String,java.lang.Object> arguments)
throws java.io.IOException其参数含义是:- queue: 用来存储消息的队列名称
- durable: 用于指定当服务器重启时,队列是否能复活.注意,当服务器重启时,如果要保证消息持久性,必须将队列声明为持久化的
- exclusive: 用于指定此队列是否只限制于当前连接.
- autoDelete:当队列不再使用时,用于指示RabbitMQ broker是否要自动删除队列.
- arguments: 这是一个可选的队列构建参数map.
在第二步中,实际上我们才会将消息发送到RabbitMQ broker.
RabbitMQ绝不打开消息体,对它来说,消息是透明的,因此你可以使用任何喜欢的序列化格式.通常我们会使用JSON, 但也可以使用XML, ASN.1, 标准的或自定义的ASCII或二进制格式. 最重要的事情是客户端程序需要知道如何来解析数据.
现在我们来深度解析 basicPublish()方法:
void basicPublish(java.lang.String exchange,java.lang.String routingKey, AMQP.BasicProperties props, byte[] body) throws java.io.IOException
在我们的例子中,exchange参数被设置成空字符串"", 即默认的交换器, routingKey 参数设置成了队列的名称. 在这种情况下,消息将直接发送到routingKey指定的队列中. body 参数设置成了字符串的字节数组,也就是我们想要发送的消息. props 参数默认设置成了null; 这些是消息属性,我们将在Using message properties中深入讨论.
在步骤3中,我们发送了完全相同的消息,但将消息属性设置成了MessageProperties.PERSISTENT_TEXT_PLAIN;通过这种方式我们要求RabbitMQ将此消息标记为持久化消息.
两个消息已经分发到了RabbitMQ broker, 逻辑上已经在myFirstQueue队列上排队了. 消息会驻留在缓冲区中,直到有一个客户端来消费(通常来说,是一个不同的客户端).
如果队列和消息都声明为持久化,消息就会被标记为持久化的,broker会将其存储在磁盘上.如果两个条件中的任何一个缺失,消息将会存储在内存中. 对于后者来说,当服务器重启时,缓冲消息将不会复活,但消息的投递和获取会更快.我们将Chapter 8, Performance Tuning for RabbitMQ来深入探讨这个主题.
更多
在本章节中,我们将讨论检查RabbitMQ状态的方法,以及队列是否存在的方法.
如何检查RabbitMQ状态
要检查RabbitMQ状态,你可以使用rabbitmqctl命令行工具.在Linux设置中,它应该在PATH环境变量中.在Windows中,可在programs |
RabbitMQ Server | RabbitMQ Command Prompt (sbin dir). 我们可从命令行提示窗口中运行rabbitmqctl.bat.
我们可以使用rbbitmqclt list_queues来检查队列状态.在下面的截屏中,显示了运行例子之前和之后的情景.
在上面的截屏中,我们可以看到myfirstqueue队列,其后跟着数字2, 它表示缓存在我们队列中的消息数目(待发送消息数目).
现在我们可以尝试重启RabbitMQ, 或者重启主机.成功重启RabbitMQ依赖于使用的OS:
在Linux, RedHat, Centos, Fedora, Raspbian上:
service rabbitmq-server restart
在Linux, Ubuntu, Debian上:
/etc/init.d/rabbitmq restart
在Windows上:
sc stop rabbitmq / sc start rabbitmq
当我们再次运行rabbitmqclt list_queues 时,能期望有多少个消息呢?
检查队列是否已经存在
要确定特定队列是否已经存在, 用channel.queueDeclarePassive()来代替channel.queueDeclare(). 两个方法在队列已经存在的情况下,会表现出相同的行为,否则,channel.queueDeclare()会创建队列,但channel.queueDeclarePassive()会抛出异常.
消费消息
在本配方中,我们将关闭此回路.我们已经知道了如何将消息发送到RabbitMQ—或者任何AMQP broker—现在,我们要学习如何获取这些消息.
你可以在Chapter01/Recipe03/src/rmqexample/ nonblocking 找到源码.
如何做
要消费前面配方中发送的消息,我们需要执行下面的步骤:
1. 声明我们要从哪里消费消息的队列:
String myQueue="myFirstQueue";
channel.queueDeclare(myQueue, true, false, false, null);
2. 定义一个继承自DefaultConsumer的消费类:
public class ActualConsumer extends DefaultConsumer {
public ActualConsumer(Channel channel) {
super(channel);
}
@Override
public void handleDelivery(String consumerTag,Envelope envelope,BasicProperties properties,byte[] body) throws java.io.IOException {
String message = new String(body);
System.out.println("Received: " + message);
}
}
3. 创建一个消费对象实例,再绑定到我们的channel上:
ActualConsumer consumer = new ActualConsumer(channel);
4. 开始消费消息:
String consumerTag = channel.basicConsume(myQueue, true,consumer);
5. 一旦完成,取消消费者(其API含义为:取消当前消费者(不能再收到此队列上的消息,重新运行消费者可以收到消息)并调用consumer的handleCancelOk方法):
channel.basicCancel(consumerTag);
如何运作
在我们建立了与AMQP broker的connection 和channel后,我们需要确保我们从哪个队列来消费消息(step 1).事实上,消费者可以在生产者发送消息到队列之前就已经启动了,此时有可能队列还不存在,为了避免队列上后续操作失败,我们需要声明队列(译者注:但消费声明队列这个动作并不必须的,只要生产者声明了队列,消费者不需要调用queueDeclare方法同样可以消费消息,在这里只能认为是一种保险措施).
TIP:
通过允许生产者和消费者声明相同的队列,我们可以解藕其存在性,同时启动的顺序也不重要.
步骤2的核心,我们通过覆盖handleDelivery()方法定义了我们特定的消费者,以及在步骤3中我们进行实例化。在Java client API中,消费者回调是通过com.rabbitmq.client.Consumer接口定义的.我们从 DefaultConsumer扩展了我们的消费者,DefaultConsumer提供了Consumer 接口所有方法中无具体操作的实现.在步骤3中,通过调用channel.basicConsume(),我们让消费者开始了消费消息.每个channel的消费者总是同一个线程上执行,而且是独立于调用者的.
现在我们已经从myQueue中激活了一个消费者,Java client library就会开始从RabbitMQ broker的队列中获取消息,并且会对每个消费者都调用handleDelivery().
在channel.basicConsume()方法调用后,我们会坐等主线程结束. 消息正在以非阻塞方式进行消费。
只有当我们按Enter之后, 执行过程会到步骤5,然后消费者退出.在这个时刻,消费者线程会停止调用我们的消费者对象,因此我们可以释放资源并退出。
更多
在本章节中,我们将了解更多关于消费者线程以及阻塞语义的用法.
更多的消费者线程
在连接定义期间,RabbitMQ Java API 会按消费者线程需要分配一个线程池。所有绑定到同一个channel的消费者都会使用线程池中的单个线程来运行;但是,有可能不同channel的消费者也可通过同一个线程来处理. 这就是为什么要在消费方法避免长时间操作的原因,为了避免阻塞其它消费者,可以在我们的自己定义的线程池中进行处理,就像我们例子中展示的一样,但这不是必须的。我们已经定义了一个线程池, java.util.concurrent.ExecutorService, 因此可在连接期间将其传入:
ExecutorService eService = Executors.newFixedThreadPool(10);
Connection connection = factory.newConnection(eService);
这是由我们来进行管理的,因此我们要负责对其终止:
eService.shutdown();
但是,必须要记住的是,如果你没有定义你自己的ExecutorService线程池,Java client library会在连接创建期间创建一个,并会在销毁对应连接时,自动销毁连接池。
使用JSON来序列化消息体(y)tion with JSON
在AMQP中,消息是不透明的实体,AMQP不提供任何标准的方式来编解码消息.但是,web应用程序经常使用JSON来作为应用程序层格式,JavaSciprt序列化格式已经变成了事实上的标准,在这种情况下,RabbitMQ client Java library 可以包含一些实用函数.另一方面,这也不是唯一的协议,任何程序可以选择它自己的协议(XML, Google Protocol Buffers, ASN.1, or proprietary).
在这个例子中,我们将展示如何使用JSON协议来编解码消息 体. 我们会使用Java编写的发布者(Chapter01/Recipe04/Java_4/src/rmqexample)来发送消息,并用 Python语言编写的消费者来消费消息 (Chapter01/Recipe04/Python04).
如何做How to do it…
要实现一个Java生产者和一个Python消费者, 你可以执行下面的步骤:
1. Java: 除了导入Connecting to the broker配方中提到的包外,我们还要导入:
import com.rabbitmq.tools.json.JSONWriter;
2. Java: 创建一个非持久化队列:
String myQueue="myJSONBodyQueue_4";
channel.queueDeclare(MyQueue, false, false, false, null);
3. Java: 创建一个使用样例数据的Book列表:
List<Book>newBooks = new ArrayList<Book>();
for (inti = 1; i< 11; i++) {
Book book = new Book();
book.setBookID(i);
book.setBookDescription("History VOL: " + i );
book.setAuthor("John Doe");
newBooks.add(book);
}
4. Java: 使用JSONwriter来序列化newBooks实例:
JSONWriter rabbitmqJson = new JSONWriter();
String jsonmessage = rabbitmqJson.write(newBooks);
5. Java: 最后发送jsonmessage:
channel.basicPublish("",MyQueue,null,jsonmessage.getBytes());
6. Python: 要使用Pika library,我们必须要导入下面的包:
import pika;
import json;
Python 有JSON处理的内键包.
7. Python: 创建RabbitMQ的连接,使用下面的代码:
connection =pika.BlockingConnection(pika.ConnectionParameters(rabbitmq_host));
8. Python: 声明队列,绑定消费者,然后再注册回调:
channel = connection.channel()
my_queue = "myJSONBodyQueue_4"
channel.queue_declare(queue=my_queue)
channel.basic_consume(consumer_callback, queue=my_queue,no_ack=True)
channel.start_consuming()
How it works…
在我们设置环境后(步骤1和步骤2),我们使用了write(newbooks)来序列化newbooks类。此方法返回返回的JSON字符串,就像下面的展示的一样:
[
{
"author" : "John Doe",
"bookDescription" : "History VOL: 1",
"bookID" : 1
},
{
"author" : "John Doe",
"bookDescription" : "History VOL: 2",
"bookID" : 2
}
]
步骤4中,我们发布了一个jsonmessage到myJSONBodyQueue_4队列中.现在Python消费者可以从同一个队列中获取消息。在Python中我们看如何操作:
connection =pika.BlockingConnection(pika.ConnectionParameters(rabbitmq_host));
channel = connection.channel()
queue_name = "myJSONBodyQueue_4"
channel.queue_declare(queue=my_queue)
..
channel.basic_consume(consumer_callback, queue=my_queue,no_ack=True)
channel.start_consuming()
正如Java实现中看到的一样,我们必须创建一个连接,然后再创建一个通道.channel.queue_declare(queue=myQueue)方法,我们声明了非持久化,不受连接限制,不会自己删除的队列。 如果要改变队列的属性,我们方法中添加参数,就像下面这样:
channel.queue_declare(queue=myQueue,durable=True)
当不同AMQP clients声明了相同队列时,那么确保有相同的durable, exclusive, 和autodelete 属性是相当重要的(如果队列名称相同,但属性不同会抛异常),否则, channel.queue_declare()会抛异常。
对于channel.basic_consume()方法, client会从给定的队列中消费消息,当接收到消息后,会调用consumer_callback()回调方法。
在Java中我们是在消费者接口中定义的回调,但在Python中,它们只是传递给basic_consume()方法, 更多的功能,更少的声明,是Python的典范.
consumer_callback回调如下:
def consumer_callback(ch, method, properties, body):
newBooks=json.loads(body);
print" Count books:",len(newBooks);
for item in newBooks:
print 'ID:',item['bookID'], '-
Description:',item['bookDescription'],' -
Author:',item['author']
回调接收到消息后,使用json.loads()来反序列化消息,然后就可以准备读取newBooks的结构了。
更多
包含在RabbitMQ client library中的JSON帮助类是非常简单的,在真实项目中,你可以使用外部JSON library.如:强大的google-gson (https://code.google.com/p/google-gson/) 或 jackson (http://jackson.codehaus.org/).
使用RPC消息
远程过程调用(RPC)通常用于client-server架构. client提出需要执行服务器上的某些操作请求,然后等待服务器响应.
消息架构试图使用发后即忘(fire-and-forget)的消息形式来实施一种完全不同的解决方案,但是可以使用设计合理的AMQP队列和增加型RPC来实施,如下所示:
上面的图形描述了request queue是与responder相关联的,reply queues 与callers是相联的.但是,当我们在使用RabbitMQ的时候,所有的涉及的端点(callers和responders) 都是AMQP clients.现在我们将描述Chapter01/Recipe05/Java_5/src/rmqexample/rpc例子中的操作步骤.
如何做
执行下面的步骤来实现RPC responder:
1. 声明一个请求队列, responder会在此处来等候RPC请求:
channel.queueDeclare(requestQueue, false, false, false,null);
2. 通过覆盖DefaultConsumer.handleDelivery()来定义我们特定的RpcResponderConsumer消费者, 在接收到每个RPC请求的时,消费者将:
执行RPC请求中的操作
准备回复消息
通过下面的代码在回复属性中设置correlation ID:
BasicProperties replyProperties = new BasicProperties.Builder().correlationId(properties.getCorrelationId()).build();
将答案发送到回复队列中:
getChannel().basicPublish("", properties.getReplyTo(),replyProperties, reply.getBytes());
发送应答给RPC request:
getChannel().basicAck(envelope.getDeliveryTag(), false);
3. 开始消费消息,直到我们看到了回复消息才停止:
现在让我们来执行下面的步骤来实现RPC caller:
1. 声明请求队列,在这里responder会等待RPC请求:
channel.queueDeclare(requestQueue, false, false, false,null);
2. 创建一个临时的,私有的,自动删除的回复队列:
String replyQueue = channel.queueDeclare().getQueue();
3. 定义我们特定的消费者RpcCallerConsumer, 它用于接收和处理RPC回复. 它将:
当收到回复时,通过覆盖handleDelivery()用于指明要做什么(在我们的例子中,定义了AddAction()):
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws java.io.IOException {
String messageIdentifier =properties.getCorrelationId();
String action = actions.get(messageIdentifier);
actions.remove(messageIdentifier);
String response = new String(body);
OnReply(action, response);
}
4. 调用channel.basicConsume()方法启动消息消费.
5. 准备和序列化请求(我们例子中是messageRequest).
6. 初始化一个任意的唯一消息标识符(messageIdentifier).
7. 当消费者收到相应回复的时候,定义应该做什么,通过使用messageIdentifier来绑定动作.在我们的例子中,我们通过调用我们自定义的方法 consumer.AddAction()来完成的.
8. 发布消息到请求队列,设置其属性:
BasicProperties props = new BasicProperties.Builder().correlationId(messageIdentifier)replyTo(replyQueue).build();
channel.basicPublish("", requestQueue,props,messageRequest.getBytes());
如何工作
在我们的例子中,RPC responder扮演的是RPC server的角色; responder会监听requestQueue公共队列(步骤1),这里放置了调用者的请求.
另一方面,每个调用者会在其私有队列上消费responder的回复信息(步骤5).当caller发送消息时(步骤11),它包含两个属性:一个是用于监听的临时回复队列 (replyTo())名称,另一个是消息标识(correlationId()),当回复消息时,用于标识caller.事实上,在我们的例子中,我们已经实现了一个异步的RPC caller. The action to be performed by the RpcCallerConsumer (step 6) when the reply comes back is recorded by the nonblocking consumer by calling AddAction() (step 10).
回到responder, RPC逻辑全在RpcResponderConsumer中.这不同于特定的非阻塞consumer,就像我们在消费消息配方中看到的一样,但不同的是下面两点细节:
回复的队列名称是通过消息属性来获取的,即properties.getReplyTo().其值已经被caller设成了私有,临时回复队列.
回复消息必须包含在correlation ID标识的队列中(待查)
TIPRPC responde不会使用correlation ID;它只用来让caller收到对应请求的回复消息
更多
本章节我们会讨论阻塞RPC的使用.
使用阻塞RPC
有时,简单性比可扩展性更重要.在这种情况下,可以使用包含在 Java RabbitMQ client library中实现了阻塞RPC语义的帮助类:
com.rabbitmq.client.RpcClient
com.rabbitmq.client.StringRpcServer
逻辑是相同的,但没有非阻塞消费者参与, 并且临时队列和correlation IDs 的处理对于用户来说是透明的.
你可以在Chapter01/Recipe05/Java_5/src/rmqexample/simplerpc找到相关的例子.
扩展注意
当有多个callers会发生什么呢?它主要以标准的RPC client/server 架构来工作.但如果运行多个reponders会怎样呢?
在这种情况下,所有的responders都会从请求队列中关注消费消息. 此外, responders可位于不同的主机. 这个主题的更多信息请参考配方-分发消息给多个消费者.
广播消息es
在本例中,我们看到如何将同一个消息发送给有可能很大量的消费者.这是一个典型的广播消息到大量客户端的消息应用.举例来说,在大型多人游戏中更新记分板的时候,或在一个社交网络应用中发布新闻的时候,都需要将消息广播给多个消费者.
在本配方中,我们同时探讨生产者和消费者实现.因为它是非常典型的消费者可以使用不同的技术和编程语言,在AMQP中,我们将使用Java, Python, 以及Ruby来展示这种互通性.
我们会感谢AMQP中隔离交换器和队列带来的好处.在Chapter01/Recipe06/中找到源码.
如何做
要做这道菜,我们需要四个不同的代码:
- Java发布者
- Java消费者
- Python消费者
- Ruby消费者
准备Java发布者:
1. 声明一个fanout类型的交换器:
channel.exchangeDeclare(myExchange, "fanout");
2. 发送一个消息到交换器:
channel.basicPublish(myExchange, "", null,jsonmessage.getBytes());
然后准备Java消费者:
1. 声明同一个生产者声明的fanout交换器:
channel.exchangeDeclare(myExchange, "fanout");
2. 自动创建一个新的临时队列:
String queueName = channel.queueDeclare().getQueue();
3. 将队列绑定到交换器上:
channel.queueBind(queueName, myExchange, "");
4. 定义一个自定义,非阻塞消费者,这部分内容已经在消费消息食谱中看到过了.
5. 调用channel.basicConsume()来消费消息
相对于Java消费者来说,Python消费者的源码是非常简单的,因此这里没必要再重复必要的步骤,只需要遵循Java消费者的步骤,可参考Chapter01/Recipe06/Python_6/PyConsumer.py的代码.
Ruby消费者中,你必须使用"bunny" 然后再使用URI连接.
可查看在Chapter01/Recipe06/Ruby_6/RbConsumer.rb的源码
现在我们要把这些整合到一起来看食谱:
1. 启动一个Java生产者的实例; 消息将立即进行发布.
2. 启动一个或多个Java/Python/Ruby的消费者实例; 消费者只有当它们运行的时候,才能接接收到消息.
3. 停止其中一个消费者,而生产者继续运行,然后再重启这个消费者,我们可以看到消费者在停止期间会丢失消息. 如何运作
生产者和消费者都通过单个连接连上了RabbitMQ,消息的逻辑路径如下图所示:
在步骤1中,我们已经声明了交换器,与队列声明的逻辑一样: 如果指定的交换器不存在,将会进行创建;否则,不做任何事情.exchangeDeclare()方法的第二个参数是一个字符串, 它用于指定交换器的类型,在我们这里,交换器类型是fanout.
在步骤2中,生产者向交换器发送了一条消息. 你可以使用下面的命令来查看它以及其它已定义的交换器:
rabbitmqctl list_exchanges
channel.basicPublish() 方法的第二个参数是路由键(routing key),在使用fanout交换器时,此参数通常会忽略.第三个设置为null的参数, 此参数代表可选的消息属性(更多信息可参考使用消息属性食谱).第四个参数是消息本身.
当我们启动一个消费者的时候,它创建一个它自己的临时队列(步骤9). 使用channel.queueDeclare()空重载,我们会创建一个非持久化,私有的,自动删除的,队列名称自动生成的队列.
运行一对消费者,并用rabbitmqctl list_queues查看,我们可以两个队列,每个消费者一个, 还有奇怪的名字,还有前面食谱中用到的持久化队列myFirstQueue ,如下图所示:
在步骤5中,我们将队列绑定到了myExchange交换器上.可以用下面的命令来监控这些绑定:
rabbitmqctl list_bindings
监控是AMQP非常重要的一面; 消息是通过交换器来路由到绑定队列的,且会在队列中缓存.
TIP
交换器不会缓存消息,它只是逻辑元素.
fanout交换器在通过消息拷贝,来将消息路由到每个绑定的队列中,因此,如果没有绑定队列,消息就不会被消费者接收(参考处理未路由消息食谱来了解更多信息).
一旦我们关闭了消费者,我们暗中地销毁了其私有临时队列(这就是为什么队列是自动删除的,否则,这些队列在未使用后会保留下来,broker上的队列数目会无限地增长), 消息也不会缓存了.
当重启消费者的时候,它会创建一个新的独立的队列,只要我们将其绑定到myExchange上,发布者发送的消息就会缓存到这个队列上,并被消费者消费.
更多
当RabbitMQ第一次启动的时候,它创建一些预定的交换器. 执行rabbitmqctl list_exchanges命令,我们可以观察到许多存在的交换器,也包含了我们在本食谱中定义的交换器:
所有出现在这里的amq.*交换器都是由AMQP brokers预先定义的,它可用来代替你定义你自己的交换器;它们不需要声明.
我们可以使用amq.fanout来替换myLastnews.fanout_6, 对于简单应用程序来说,这是很好的选择. 但一般来说,应用程序来声明和使用它们自己的交换器.
本食谱使用的重载,交换器是非自动删除的(won't be deleted as soon as the last client detaches it) 和非持久化的(won't survive server restarts). 你可以在http://www.rabbitmq.com/releases/ rabbitmq-java-client/current-javadoc/找到更多的选项和重载.
使用Direct交换器来路由消息
要本食谱中,我们将看到如何选择消费消息子集(部分消息), 只路由那些感涂在的AMQP队列,以及忽略其它队列.
一个典型的使用场景是实现一个聊天器, 在这里每个队列代表了一个用户.我们可以查看下面的目录找到相关的例子:Chapter01/Recipe07/Java_7/src/rmqexample/direct
我们将展示如何同时实现生产者和消费者.实现生产者,执行下面的步骤:
1. 声明一个direct交换器:
channel.exchangeDeclare(exchangeName, "direct", false,false, null);
2. 发送一些消息到交换器,使用任意的routingKey 值:
channel.basicPublish(exchangeName, routingKey, null,jsonBook.getBytes());
要实现消费者,执行下面的步骤:
1. 声明同样的交换器,步骤与上面步骤相同.
2. 创建一个临时队列:
String myQueue = channel.queueDeclare().getQueue();
3. 使用bindingKey将队列绑定到交换器上. 假如你要使用多个binding key,可按需要多次执行这个操作:
channel.queueBind(myQueue,exchangeName,bindingKey);
4. 在创建了适当的消费对象后,可以参考消费消息食谱来消费消息.
如何工作
在本食谱中,我们使用任意的字符串(也称为路由键)来向direct交换器发布消息(step 2).在fanout交换器中,如果没有绑定队列的话,消息是不是存储的,但在这里,根据在绑定时指定的绑定键,消费者可以选择消息转发这些队列(步骤5).
仅当路由键与绑定键相同的消息才会被投递到这些队列.
TIP
过滤操作是由AMQP broker来操作,而不是消费者;路由键与绑定键不同的消息是不会放置到队列中的.但是,可允许多个队列使用相同的绑定键,broker会将匹配的消息进行拷贝,并投递给它们.也允许在同一个队列/交换绑定上绑定多个不同的绑定键,这样就可以投递所有相应的消息.
更多
假如我们使用指定的路由键来将消息投递到交换器,但在这个指定键上却没有绑定队列,那么消息会默默的销毁.
然而, 当发生这种情况时,生产者可以检测这种行为,正如处理未路由消息食谱中描述的一样.
使用topic交换器来路由消息
Direct 和topic 交换器在概念上有点相似,最大的不同点是direct交换器使用精准匹配来选择消息的目的地,而topic交换器允许使用通配符来进行模式匹配.
例如, BBC使用使用topic交换器来将新故事路由到恰当的RSS订阅.
你可以在这里找到topic交换器的例子:Chapter01/Recipe08/Java_8/src/rmqexample/topic
如何做
我们先从生产者开始:
1. 声明一个topic交换器:
channel.exchangeDeclare(exchangeName, "topic", false,false, null);
2. 使用任意的路由键将消息发送到交换器:
channel.basicPublish(exchangeName, routingKey, null,jsonBook.getBytes());
接下来,消费者:
1. 声明相同的交换,如步骤1做的一样.
2. 创建一个临时队列:
String myQueue = channel.queueDeclare().getQueue();
3. 使用绑定键将队列绑定到交换器上,这里也可以包含通配符:
channel.queueBind(myQueue,exchangeName,bindingKey);
4. 在创建适当的消费者对象后,可以像消息消息食谱中一样来消费消息.
如何工作
以先前的食谱中,用字符串标记来将消息发送到topic交换器中(步骤2),但对于topic交换器来说,组合多个逗号分隔的单词也是很重要的;它们会被当作主题消息.例如,在我们的例子中,我们用:
technology.rabbitmq.ebook
sport.golf.paper
sport.tennis.ebook
要消息这些消息,消费者需要将myQueue绑定到交换器上(步骤5)
使用topic交换器, 步骤5中指定的订阅绑定/绑定键可以是一系列逗号分隔的单词或通配符. AMQP通配符只包括:
- #: 匹配0或多个单词
- *: 只精确匹配一个单词
例如:
- #.ebook 和 *.*.ebook 可匹配第一个和第三个发送消息
- sport.# and sport.*.* 可匹配第二个和第三个发送消息
- # 可匹配任何消息
在最后一种情况中,topic交换器的行为类似于fanout交换器, 但性能不同,当使用这种形式时性能更高
更多
再次说明,如果消息不能投递到任何队列,它们会被默默地销毁.当发生此种情况时,生产者可以检测这种行为,就如处理未路由消息食谱中描述的一样.
保证消息处理
在这个例子中,我们将展示在消费消息时,我们如何来使用明确的应答.消息在消费者获取并对broker作出应答前,它会一直存在于队列中.应答可以是明确的或隐含的.在先前的例子中,我们使用的是隐含应答.为了能实际查看这个例子,你可以运行生产消息食谱中的发布者,然后你运行消费者来获取消息,可在Chapter01/Recipe09/Java_9/中找到.
如何做
为了能保证消费者处理完消息后能应答消息,你可以执行下面的步骤:
1. 声明一个队列:
channel.queueDeclare(myQueue, true, false, false,null);
2. 绑定消费者与队列,并设置basicConsume()方法的autoAck参数为false:
ActualConsumer consumer = new ActualConsumer(channel);
boolean autoAck = false; // n.b.
channel.basicConsume(MyQueue, autoAck, consumer);
3. 消费消息,并发送应答:
public void handleDelivery(String consumerTag,Envelope envelope, BasicPropertiesproperties,byte[] body) throws java.io.IOException {
String message = new String(body);
this.getChannel().basicAck(envelope.getDeliveryTag(),false);
}
如何工作
在创建队列后(步骤1),我们将消费者加入到队列中,并且定义了应答行为(步骤2).
参数autoack = false表明RabbitMQ client API会自己来发送明确的应答.
在我们从队列收到消息后,我们必须向RabbitMQ发送应答,以表示我们收到到消息并适当地处理了,因此我们调用了channel.basicAck()(步骤3).
RabbitMQ只有在收到了应答后,才会从队列中删除消息.
TIP
如果在消费者不发送应答,消费者会继续接收后面的消息;但是,当你断开了消费者后,所有的消息仍会保留在队列中.消息在RabbitMQ收到应答前,都认为没有被消费.可以注解basicAck()调用来演示这种行为.
channel.basicAck()方法有两个参数:
- deliveryTag
- multiple
deliveryTag参数是由服务器为消息指定的值,你可以通过使用delivery.getEnvelope().getDeliveryTag()来获取.
如果multiple设置为false,client只会应答deliveryTag参数的消息, 否则,client会应答此消息之前的所有消息. 通过向RabbitMQ应答一组消息而不是单个消息,此标志允许我们优化消费消息. TIP
消息只能应答一次,如果对同一个消息应答了多次,方法会抛出preconditionfailed 异常.
调用channel.basicAck(0,true),则所有未应答的消息都会得到应答,0 代表所有消息.此外,调用channel.basicAck(0,false) 会引发异常.
更多
下面的章节,我们还会讨论basicReject()方法,此方法是RabbitMQ扩展,它允许更好的灵活性.
也可参考
分发消息到多个消费者食谱是一个更好解释明确应答真实例子.
分发消息到多个消费者
在这个例子中,我们将展示如何来创建一个动态负责均衡器,以及如何将消息分发到多个消费者.我们将创建一个文件下载器.
你可在Chapter01/Recipe10/Java_10/找到源码.
如何做
为了能让两个以上的RabbitMQ clients能尽可能的负载均衡来消费消息,你必须遵循下面的步骤:
1. 声明一个命令队列, 并按下面这样指定basicQos:
channel.queueDeclare(myQueue, false, false, false,null);
channel.basicQos(1);
2. 使用明确应答来绑定一个消费者:
channel.basicConsume(myQueue, false, consumer);
3. 使用channel.basicPublish()来发送一个或多个消息.
4. 运行两个或多个消费者.
如何工作
发布者发送了一条带下载地址的消息:
String messageUrlToDownload="http://www.rabbitmq.com/releases/rabbitmq-dotnetclient/v3.0.2/rabbitmq-dotnet-client-3.0.2-user-guide.pdf";
channel.basicPublish("",MyQueue,null,messageUrlToDownload.getBytes());
消费者获取到了这个消息:
System.out.println("Url to download:" + messageURL);
downloadUrl(messageURL);
一旦下载完成,消费者将向broker发送应答,并开始准备下载下一个:
getChannel().basicAck(envelope.getDeliveryTag(),false);
System.out.println("Ack sent!");
System.out.println("Wait for the next download...");
消费者按块的方式能获取消息,但实际上,当消费者发送应答时,消息就会从队列中删除,在先前的食谱中,我们已经看过这种情况了.
另一个方面,在本食谱中使用了多个消费才,第一个会预先提取消息,其它后启动的消费者在队列中找不到任何可用的消息.为了平等地发分发消息,我们需要使用channel.basicQos(1)来指定一次只预先提取一个消息.
也可参考
在Chapter 8, Performance Tuning for RabbitMQ中可以找到更多负载均衡的信息.
使用消息属性
在这个例子中,我们将展示如何AMQP消息是如何分解的,以及如何使用消息属性.
你可在Chapter01/Recipe11/Java_11/找到源码.
如何做
要访问消息属性,你必须执行下面的步骤:
1. 声明一个队列:
channel.queueDeclare(MyQueue, false, false, false,null);
2. 创建一个BasicProperties类:
Map<String,Object>headerMap = new HashMap<String,Object>();
headerMap.put("key1", "value1");
headerMap.put("key2", new Integer(50) );
headerMap.put("key3", new Boolean(false));
headerMap.put("key4", "value4");
BasicProperties messageProperties = new BasicProperties.Builder()
.timestamp(new Date())
.contentType("text/plain")
.userId("guest")
.appId("app id: 20")
.deliveryMode(1)
.priority(1)
.headers(headerMap)
.clusterId("cluster id: 1")
.build();
3. 使用消息属性来发布消息:
channel.basicPublish("",myQueue,messageProperties,message.getBytes())
4. 消费消息并打印属性:
System.out.println("Property:" + properties.toString());
如何工作
AMQP 消息(也称为内容)被分成了两部分:
- 内容头
- 内容体(先前例子我们已经看到过了)
在步骤2中,我们使用BasicProperties创建一个内容头:
Map<String,Object>headerMap = new HashMap<String, Object>();
BasicProperties messageProperties = new BasicProperties.Builder()
.timestamp(new Date())
.userId("guest")
.deliveryMode(1)
.priority(1)
.headers(headerMap)
.build();
在这个对象中,我们设置了下面的属性:
- timestamp: 消息时间戳.
- userId: 哪个用户发送的消息(默认是"guest"). 在下面的章节中,我们将了解用户管理.
- deliveryMode: 如果设置为1,则消息是非持久化的, 如果设置为2,则消息是持久化的(你可以参考食谱连接broker).
- priority: 用于定义消息的优先级,其值可以是0到9.
- headers: 一个HashMap<String, Object>头,你可以在其中自由地定义字段.
TIP RabbitMQ BasicProperties 类是一个AMQP内容头实现.BasicProperties的属性可通过BasicProperties.Builder()构建.头准备好了,我们可使用
channel.basicPublish("",myQueue, messageProperties,message.getBytes())来发送消息,在这里,messageProperties是消息头,message是消息体.
在步骤中,消费者获得了一个消息:
public void handleDelivery(String consumerTag,Envelope envelope,BasicProperties properties,byte[] body) throws java.io.IOException {
System.out.println("***********message header****************");
System.out.println("Message sent at:"+ properties.getTimestamp());
System.out.println("Message sent by user:"+ properties.getUserId());
System.out.println("Message sent by App:"+properties.getAppId());
System.out.println("all properties :" + properties.toString());
System.out.println("**********message body**************");
String message = new String(body);
System.out.println("Message Body:"+message);
}
参数properties包含了消息头,body包含了消息体.
更多
使用消息属性可以优化性能.将审计信息或日志信息写入body,通常是一种典型的错误,因为消费者需要解析body来获取它们.
body 消息只可以包含应用程序数据(如,一个Book class),而消息属性可以持有消息机制相关或其它实现细节的相关信息.
例如 ,如果消费者想知道消息是何时发送的,那么你可以使用timestamp属性, 或者消费者需要根据一个定制标记来区分消息,你可以将它们放入header HashMap属性中.
也可参考
MessageProperties类对于标准情况,包含了一些预先构建的BasicProperties类. 可查看http://www.rabbitmq.com/releases//rabbitmq-java-client/current-javadoc/com/rabbitmq/client/
MessageProperties.html
在这个例子中,我们只是使用了一些属性,你可在http://www.rabbitmq.com/releases//rabbitmq-java-client/currentjavadoc/com/rabbitmq/client/AMQP.BasicProperties.html获取更多信息.
消息事务
在本例中,我们将讨论如何使用channel事务. 在生产消息食谱中,我们已经了解了如何来使用持久化消息,但如果broker不能将消息写入磁盘的话,那么你就会丢失消息.使用AQMP事务,你可以确保消息不会丢失.
你可在Chapter01/Recipe12/Java_12/找到相关源码.
如何做
通过下面的步骤,你可以使用事务性消息:
1. 创建持久化队列
channel.queueDeclare(myQueue, true, false, false, null);
2. 设置channel为事务模式:
channel.txSelect();
3. 发送消息到队列,然后提交操作:
channel.basicPublish("", myQueue,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
channel.txCommit();
如何工作
在创建了持久化队列后(step 1),我们将channel设置成了事务模式,使用的方法是txSelect() (step 2). 使用 txCommit()确保消息存储在队列并写入磁盘,然后消息将投递给消费者.在txCommit() 或txRollback()之前,必须至少调用一次txSelect().
在一个DBMS中,你可以使用回滚方法.在下面的情况下,消息不会被存储或投递:
channel.basicPublish("",myQueue,MessageProperties.PERSISTENT_TEXT_PLAIN ,message.getBytes());
channel.txRollback();
更多
事务会降低应用程序的性能,因为broker不会缓存消息,且tx操作是同步的.
也可参考
在后面的章节中,我们会讨论发布确认插件,这是一种较快确认操作的方式.
处理未路由消息
在这个例子中,我们将展示如何管理未路由的消息. 未路由消息指的是没有目的地的消息.如,一个消息发送到了无任何绑定队列的交换器上.
未路由消息不同于死消息 ,前者是发送到无任何队列目的地的交换器上,而后者指的是消息到达了队列,但由于消费者的决策,过期TTL,或者超过队列长度限制而被拒绝的消息 你可以在Chapter01/Recipe13/Java_13/找到源码.
如何做
为了处理未路由的消息,你需要执行下面的操作:
1. 第一步实现ReturnListener接口:
public class HandlingReturnListener implements ReturnListener
@Override
public void handleReturn…
2. 将HandlingReturnListener类添加到channel.addReturnListener():
channel.addReturnListener(new HandlingReturnListener());
3. 然后创建一个交换机:
channel.exchangeDeclare(myExchange, "direct", false, false,null);
4. 最后发布一个强制消息到交换器:
boolean isMandatory = true;
channel.basicPublish(myExchange, "",isMandatory, null,message.getBytes());
如何工作
当我们运行发布者的时候,发送到myExchange的消息因为没有绑定任何队列不会到达任何目的地.但这些消息不会,它们会被重定向到一个内部队列. .HandlingReturnListener类会使用handleReturn()来处理这些消息.ReturnListener类绑定到了一个发布者channel上, 且它会猎捕那些不能路由的消息.
在源码示例中,你可以找到消费者,你也可以一起运行生产者和消费者,然后再停止消费者.
更多
如果没有设置channel ReturnListener, 未路由的消息只是被broker默默的抛弃.在这种情况下,你必须注意未路由消息,将mandatory 标记设置为true是相当重要的,如果为false,未路由的消息也会被抛弃.
posted on 2016-06-03 23:22
胡小军 阅读(2588)
评论(0) 编辑 收藏 所属分类:
RabbitMQ