一、远程调用说明
关于远程调用,Remote Procedure Call 简称 RPC,大家并不陌生,有时候我们需要远程的一个程序去处理数据,再把处理结果传给我们。这里我们就来实现RabbitMQ的远程调用:
客户端请求远程服务器计算斐波拉契数列和,并回传计算结果。 |
实现算法如下:
//计算斐波拉契和
private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
我们打算将在客户端中写一个call方法,实现调用并返回结果,形式如下:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
远程调用也存在一些问题:如程序不知道调用的function是本地的,还是缓慢的RPC。再者,使用远程调用会给调试带来困难。故对于远程调用,RabbitMQ给出了一些建议:弄清楚方法是本地的,还是远程调用的;通过文档标记展示清楚组件间的依赖关系;能够使用异步的尽量使用异步操作。 |
二、Callback Queue
既然是远程调用的队列,那么我们需要回传处理结果,就需要创建用于请求远程调用且能够带出处理结果的队列,我们使用默认队列(独占的)
String callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties
.Builder()
.replyTo(callbackQueueName)
.build();
channel.basicPublish("", "rpc_queue", props, message.getBytes());
三、消息属性
在上面的代码中,我们发送消息时设置了一些属性,回顾在Work模式的讲解中,我们给消息设置为持久属性的代码:
channel.basicPublish("", "task_queue",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
由于远程调用是基于AMQP 0-9-1协议实现的,在远程调用的时候,我们同样需要设置一些AMQP 0-9-1要求的属性,一共有14个属性,不过由于大部分都不常用,我们只列举4中最常用的属性:
① deliveryMode:标记消息是持久,还是暂时消息,对应我们Work模式中所用到的。
② contentType:指定编码的MIME类型,比如要使用json数据,便可指定为:application/json
③ replyTo:通常用于指定Callback Queue的名称
④ correlationId:用于远程调用的request与response之间的关系标识
四、详解correlationId
为了提高程序的效率,我们打算给每一个客户端都创建一个Callback Queue,但是这会带来新的问题:服务器并不知到某个request属于哪一个客户端。
所以我们就必须给每个request一个唯一标识,这个标识便是correlationId,也就是request与服务器之间的关系标识。
这样我们便可以大胆的使用多个客户端,并且也不会与服务器之间造成紊乱了。
由于服务器 |
五、过程总结
我们将远程调用的过程总结一下:
① 一个客户端发起请求,请求中带有两个标识:replyTo用于指定一个匿名的,独占的队列用于发起请求;correlationId用于标识该请求的唯一身份。
② 请求发送到rpc_queue队列。
③ 服务器等待接收请求,当接收到请求时便进行处理,并将处理结果返回给客户端,使用replyTo指定的队列名。
④ 客户端等待计算结果,一旦收到消息便检查,消息中的属性:correlationId,如果匹配,则取出结果数据。
六、代码实现
服务端:
import com.rabbitmq.client.*;
import java.io.IOException;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
//计算斐波拉契和
private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = null;
try {
connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
//清空队列
channel.queuePurge(RPC_QUEUE_NAME);
channel.basicQos(1);
System.out.println("wait for request...");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String correlationId = properties.getCorrelationId();
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(correlationId)
.build();
StringBuffer response = new StringBuffer();
String message = new String(body, "utf-8");
int n = Integer.parseInt(message);
System.out.println("fib(" + message + ")...");
response.append(fib(n));
channel.basicPublish("", properties.getReplyTo(), replyProps, response.toString().getBytes("UTF-8"));
//手动回复
channel.basicAck(envelope.getDeliveryTag(), false);
synchronized (this) {
this.notify();
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
while (true) {
synchronized (consumer) {
try {
consumer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
}
}
}
}
客户端:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
public class RpcClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
public RpcClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
}
public String call(String message) throws IOException, InterruptedException {
String correlateId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(correlateId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
//阻塞队列,大小为1
BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//如果唯一标识对应,则以非阻塞的形式向阻塞队列中加入服务器返回的结果
if (properties.getCorrelationId().equals(correlateId)) {
response.offer(new String(body, "UTF-8"));
}
}
};
String consumerTag = channel.basicConsume(replyQueueName, true, consumer);
//以阻塞的形式在阻塞队列中取出结果
String result = response.take();
//清除消费者,下次不会再往该消费者发送消息
channel.basicCancel(consumerTag);
return result;
}
public void close() throws IOException, TimeoutException {
if(channel!=null){
channel.close();
}
if(connection!=null){
connection.close();
}
}
public static void main(String[] args) {
RpcClient client = null;
String response = null;
try {
client = new RpcClient();
for (int i = 0; i < 32; i++) {
String i_str = Integer.toString(i);
System.out.println("请求计算:fib(" + i + ")");
response = client.call(i_str);
System.out.println("得到计算结果:" + response);
}
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
} finally {
if (client != null) {
try {
client.close();
} catch (Exception e) {
}
}
}
}
}
运行结果:
服务端:由于我们可能有多个服务端,为了保证负载均衡,我们要设置basicQos的prefetchCount 为1。
客户端端:首先我们需要产生一个唯一的标识,我们需要这个值去匹配正确的response;然后我们需要创建一个独占的Callback Queue去发送请求与等待回复;接下来我们带着两个属性replyTo与correlationId去发起请求;这个时候,只需要等待服务器的返回结果。
由于我们是使用在回调函数handleDelivery中去接收服务器传回的结果,是在子线程中进行的,而我们在主线程处理得到的结果,这里使用BlockQueue来进行处理,就是十分方便。
handleDelivery方法只需要做一件简单的事情:对于服务器传回的每一个结果,检查其correlationId是否是我们期望的,如果是期望的就把该结果放入到BlockingQueue中。
而主线程需要做的则是:从BlockingQueue取出结果,展现给用户。
关于BlockingQueue的用法,建议大家参考博客:
https://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html
大家也可以自行尝试使用多个客户端运行。
ok,到此我们RabbitMQ远程调用就结束了,
还有更多的关于RabbitMQ的用法:linux下的搭建,以及其余Spring的整合,就需要大家自行查资料了,也推荐大家使用RabbitMQ的官方网站:http://www.rabbitmq.com/
谢谢大家的观看!