[一曲广陵不如晨钟暮鼓]
本文,我们来介绍RabbitMQ中的RPC远程调用。在正式开始之前,我们假设RabbitMQ服务已经启动,运行端口为5672,如果各位看官有更改过默认配置,那么就需要修改为对应端口,保持一致即可。
准备工作:
操作系统:window 7 x64
其他软件:eclipse mars,jdk7,maven 3
--------------------------------------------------------------------------------------------------------------------------------------------------------
在前文介绍的关于“工作队列”的教程中,我们演示了如何在多个接受这之间分发资源密集型的任务。接下来,我们将继续深入的讨论这个问题。
如果我们想要在一台远程的机器上运行一个资源密集型的任务,那么是不是就意味着需要等待返回结果呢?本文,我们就来介绍RabbitMQ中的RPC远程调用模式的概念及使用。
在下文中,我们将会演示构建一个远程调用系统:一个客户端,一个可伸缩的RPC服务器。由于我们并没有实际的资源密集型的任务,所以我们打算假装执行一个RPC服务,实际上却是返回一个斐波那契数列。
为了说明RPC服务是如何被使用的,我们将创建一个简单的客户端,其将负责暴露一个call方法,并且其会堵塞进程,直到接收到返回值。示例如下:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
尽管在系统中,RPC调用时非常普遍存在的模式,但其却常被人们所诟病。问题发生在:当程序开发人员没有及时的注意到一个服务是被本地调用的,或者,远程调用过程非常的缓慢。类似于这样的情况发生在不可预测的系统环境中,并且,为了测试系统,增加了不必要的测试复杂度。本应该简化的软件,由于滥用RPC导致了在系统中增加了大量不可维护的代码。
铭记上面的问题,我们给出以下的几点建议:
当存在疑问时,尽量的避免的使用RPC调用。如果条件允许的话,推荐使用异步的消息管道---而不是RPC---效果类似于阻塞,最终异步调用被延迟到下一个计算过程(意译为调度过程,方便理解)。
一般来讲,在RabbitMQ上搭建RPC调用框架是非常容易的---客户端发送请求消息,服务端回应消息。为了接收响应消息,我们需要在发送请求时,附带一个回调队列地址。可以使用默认队列(Java客户端特有的),具体如下:
callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties
.Builder()
.replyTo(callbackQueueName)
.build();
channel.basicPublish("", "rpc_queue", props, message.getBytes());
// ... then code to read a response message from the callback_queue ...
AMQP协议预定义了14个消息属性。其中的大部分都是很少用到的,但是下面的几个,希望各位看官能够牢记:
综上,我们需要在类中引入下面这句话:
import com.rabbitmq.client.AMQP.BasicProperties;
在上面介绍的方法中,我们暗示了需要为每个RPC请求创建一个回调队列。这种做法显然是非常低效率的,但幸运的是,有一种更好的方式供我们选择---我们可以为所有的客户端之创建一个回调队列。
但是,这种做法又带来的新的问题,队列接收到一个响应时,无法确定其归属于哪一个请求。这正是关联ID发挥作用的时机。我们将为每一个请求与返回之间设置一个唯一的关联ID。之后,当回调队列中接收到响应时,我们再来观察该属性,并且基于它,我们就有办法将请求与响应进行匹配。如果我们发现一个未知的关联ID,就可以在保证安全的前提下,丢弃这条消息---因为其不属于我们已经记录下的所发出的请求。
各位看官可能会问:为什么我们可以忽略掉队列当中未知的消息,而不是产生一条错误。这是因为:在服务器上发生竞争条件的可能性,尽管很小很小,但仍然有可能发生:RPC服务器,在我们刚发送完响应之后,发生宕机,但还没来得及向请求方进行消息确认。如果这种情况发生了,重启RPC调用将会再次发起这个请求。这就是为什么在客户端上,我们就必须的完全的处理好重复响应,并且,RPC服务最好是幂等性的。
我们的RPC将会类似下面的流程进行工作:
综上所述,我们来看看完整的工程吧,具体内容如下:
1.修改pom文件,具体内容请看前文,在此不再赘述。
2.创建RPCClient文件,具体内容如下:
package com.csdn.ingo.rabbitmq_1;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer;
public RPCClient() throws Exception {
//• 先建立一个连接和一个通道,并为回调声明一个唯一的'回调'队列
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(AMQP.PROTOCOL.PORT);
connection = factory.newConnection();
channel = connection.createChannel();
//• 注册'回调'队列,这样就可以收到RPC响应
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
}
//发送RPC请求
public String call(String message) throws Exception {
String response = null;
String corrId = java.util.UUID.randomUUID().toString();
//发送请求消息,消息使用了两个属性:replyto和correlationId
BasicProperties props = new BasicProperties.Builder()
.correlationId(corrId).replyTo(replyQueueName).build();
channel.basicPublish("", requestQueueName, props, message.getBytes());
//等待接收结果
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//检查它的correlationId是否是我们所要找的那个
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response = new String(delivery.getBody());
break;
}
}
return response;
}
public void close() throws Exception {
connection.close();
}
}
3.创建RPCMain文件,具体内容如下:
package com.csdn.ingo.rabbitmq_1;
public class RPCMain {
public static void main(String[] args) throws Exception {
RPCClient rpcClient = new RPCClient();
System.out.println(" [x] Requesting getMd5String(abc)");
String response = rpcClient.call("abc");
System.out.println(" [.] Got '" + response + "'");
rpcClient.close();
}
}
4.创建RPCServer文件,具体内容如下:
package com.csdn.ingo.rabbitmq_1;
import java.security.MessageDigest;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
public static void main(String[] args) throws Exception {
//• 先建立连接、通道,并声明队列
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(AMQP.PROTOCOL.PORT);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
//•可以运行多个服务器进程。通过channel.basicQos设置prefetchCount属性可将负载平均分配到多台服务器上。
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
//打开应答机制autoAck=false
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
System.out.println(" [x] Awaiting RPC requests");
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
BasicProperties props = delivery.getProperties();
BasicProperties replyProps = new BasicProperties.Builder()
.correlationId(props.getCorrelationId()).build();
String message = new String(delivery.getBody());
System.out.println(" [.] getMd5String(" + message + ")");
String response = getMd5String(message);
//返回处理结果队列
channel.basicPublish("", props.getReplyTo(), replyProps,
response.getBytes());
//发送应答
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
// 模拟RPC方法 获取MD5字符串
public static String getMd5String(String str) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
char[] charArray = str.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16)
hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
}
5.测试方法,首先启动Server,在运行main方法,观察控制台输出即可。
6.特别备注:
上面这份源码,摘自其他博文,再次表示感谢。
我们没有斐波那契数列作为演示,但原理一致,有兴趣的看官可以在官方文档中找到源码,自行测试即可。
上面源码中使用的方法在前文中均有解释,有疑问的地方,请各位看官自行查看。
--------------------------------------------------------------------------------------------------------------------------------------------------------
至此,系统拆分解耦利器之消息队列---RabbitMQ-RPC远程调用 结束
参考资料:
官方文档:http://www.rabbitmq.com/tutorials/tutorial-six-java.html