PS:本教程的程序代码有些地方可能与原文的稍有不同。使用的amqp-client.jar版本为5.2.0。
原文来自:RabbitMQ Tutorials
前提
该教程假定你已经安装了RabbitMQ,并且已经在 localhost 中运行起来。没安装的可参考:RabbitMQ安装
介绍
RabbitMQ是一个消息代理,用于接受并转发消息。你可以把它想象成一个邮局:当你把想要邮寄的信件放到邮箱,邮递员最终会把信件送到收件人手中。在这个比喻中,RabbitMQ就相当于邮箱、邮局和邮递员。
RabbitMQ与邮局的区别在于RabbitMQ不处理纸质信件,而是接受、存储并转发二进制数据块──消息。
RabbitMQ的消息发送过程中,通常使用如下一些术语:
-
生产者:生产相当于发送,发送消息的程序就是生产者。
-
队列:队列相当于RabbitMQ的邮箱。尽管消息只是在RabbitMQ和你的程序直接流转,但是消息只能存储在队列中。队列仅受主机内存和磁盘限度的约束,本质上是一个大型的消息缓冲区。多个生产者可以发送消息到同一个队列中,多个消费者也可以尝试从同一个队列中接收数据。
-
消费者:消费和接受具有相似的意思,通常等待接受消息的程序就是消费者。
注意,生产者、消费者以及消息代理(RabbitMQ)并不一定要在同一个主机上;实际上大部分程序也是如此。一个程序也可以同时是消费者和生产者。
"Hello World"
在这一部分中,我们将会用Java编写两个程序:用于发送单条消息的生产者、一个用于接受并打印出消息的消费者。专注于这个简单的入门程序,我们将会忽略Java API的一些细节描述。程序中的消息是"Hello World"。
下图中,"P"代表生产者,"C"代表消费者,中间的小盒子代表队列──RabbitMQ为消费者而保持的一个消息缓冲区。
程序开始前,我们需要引入Java客户端依赖amqp-client.jar,也可以通过maven的引入:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.2.0</version>
</dependency>
发送(生产者)
我们将消息的发布者(生产者)命名为Send,将消息的消费者命名为Recv。发布者将会连接到RabbitMQ,发送一条消息,然后退出。
在 Send.java,我们需要引入一些类:
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
创建class并定义队列的名称:
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
...
}
}
然后创建一个连接,连接到服务器:
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
}
这里我们连接到的是本机机器的一个消息代理(RabbitMQ)──因此为localhost。如果我们想要连接到其他机器上的代理,只要指定它的名字或者IP即可。
接下来我们创建了一个通道(channel),大部分的处理数据的API在这里完成。注意,在这里我们可以使用 try-with-resources 语句创建Connection 以及Channel,这样我们就不用写代码对其进行关闭。
对于Send来说,我们必须声明消息要发到哪个队列:然后我们可以把消息发布到队列中,这些都在 try-with-resources 里面进行:
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
队列的定义是幂等(其任意多次执行所产生的影响均与一次执行的影响相同)的,只有在队列不存在时才会创建。消息的内容是一个字节数组,所以你可以随意进行编码。
完整代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send {
private final static String QUEUE_NAME = "hello";
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.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
接收(消费者)
消费者从RabbitMQ中接收生产者推送过来的消息,所以不同于发布者只发送一条消息就退出,我们让消费者一直保持对消息的监听,并把接收到的消息打印出来。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
与发布者相同,我们打开一个连接和一个通道,并声明我们将要消耗的队列。注意,队列名称与Send中声明的队列名称匹配。
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
}
}
注意,这里我们同样声明了队列,因为我们可能在启动生产者应用之前先启动了消费者应用,我们想确保在从一个队列消费消息前这个队列是存在的。
如果我们先启动消费者应用,并且让其监听一个不存在的队列,会报如下错误:
Exception in thread "main" java.io.IOException
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:126)
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:122)
at com.rabbitmq.client.impl.ChannelN.basicConsume(ChannelN.java:1369)
at com.rabbitmq.client.impl.recovery.AutorecoveringChannel.basicConsume(AutorecoveringChannel.java:540)
at com.rabbitmq.client.impl.recovery.AutorecoveringChannel.basicConsume(AutorecoveringChannel.java:494)
at com.rabbitmq.client.impl.recovery.AutorecoveringChannel.basicConsume(AutorecoveringChannel.java:477)
at Recv.main(Recv.java:23)
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'hel' in vhost '/', class-id=60, method-id=20)
at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:494)
at com.rabbitmq.client.impl.ChannelN.basicConsume(ChannelN.java:1363)
... 4 more
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'hel' in vhost '/', class-id=60, method-id=20)
at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:510)
at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:346)
at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:178)
at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:111)
at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:670)
at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:48)
at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:597)
at java.lang.Thread.run(Thread.java:748)
为什么这里我们不使用try-with-resource 去自动关闭channel和connection?因为我们希望在消费者异步收听消息到达时,该进程保持活动状态。
接下来我们将告诉服务器从队列中把消息传送个我们。因为它会异步地向我们发送消息,所以我们以对象的形式提供一个回调用于缓冲消息,直到我们准备好使用它们。这就是DeliverCallback子类的作用。
/**
*
* JDK1.8及以上
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
/**
*
*JDK版本低于1.8使用此代码
*
*/
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
完整代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
/*JDK版本低于1.8使用此代码
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
}
}
运行结果
当我们每次运行生产者程序时,消费者就会打印出接受到的消息。