摘要: 原创出处 blog.csdn.net/u013256816/article/details/55218595 「朱小厮」欢迎转载,保留摘要,谢谢!
什么是RPC?
RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用,
RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。
RabbitMQ怎么实现RPC调用?
Callback Queue
一般在RabbitMQ中做RPC是很简单的。客户端发送请求消息,服务器回复响应的消息。为了接受响应的消息,我们需要在请求消息中发送一个回调队列。可以使用默认的队列(which is exclusive in the java client.):
callbackQueueName = channel.queueDeclare().getQueue(); BasicProperties props = new BasicProperties.Builder().replyTo(callbackQueueName).build(); channel.basicPublish("" , "rpc_queue" ,props,message.getBytes());
Message properties
AMQP协议为消息预定义了一组14个属性。
private String contentType;private String contentEncoding;private Map<String,Object> headers;private Integer deliveryMode;private Integer priority;private String correlationId;private String replyTo;private String expiration;private String messageId;private Date timestamp;private String type;private String userId;private String appId;private String clusterId;
大部分的属性是很少使用的。除了以下几种(其余有兴趣可以自行查看):
deliveryMode: 标记消息传递模式,2-消息持久化,其他值-瞬态。
contentType:内容类型,用于描述编码的mime-type. 例如经常为该属性设置JSON编码。
replyTo:应答,通用的回调队列名称,
correlationId:关联ID,方便RPC相应与请求关联。
Correlation Id
在上述方法中为每个RPC请求创建一个回调队列。这是很低效的。幸运的是,一个解决方案:可以为每个客户端创建一个单一的回调队列。
新的问题被提出,队列收到一条回复消息,但是不清楚是那条请求的回复。这是就需要使用correlationId属性了。我们要为每个请求设置唯一的值。然后,在回调队列中获取消息,查看这个属性,关联response和request就是基于这个属性值的。如果我们看到一个未知的correlationId属性值的消息,可以放心的无视它——它不是我们发送的请求。
你可能问道,为什么要忽略回调队列中未知的信息,而不是当作一个失败?这是由于在服务器端竞争条件的导致的。虽然不太可能,但是如果RPC服务器在发送给我们结果后,发送请求反馈前就挂掉了,这有可能会发送未知correlationId属性值的消息。如果发生了这种情况,重启RPC服务器将会重新处理该请求。这就是为什么在客户端必须很好的处理重复响应,RPC应该是幂等的。
Summary
RPC的处理流程:
当客户端启动时,创建一个匿名的回调队列。
客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。
请求被发送到rpc_queue队列中。
RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。
Demo Code
这里采用官网的一个例子来说明,RPC客户端通过RPC调用服务器来计算斐波那契额值。
首先是服务端的代码:
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(RabbitConfig.ip); factory.setPort(RabbitConfig.port); factory.setUsername(RabbitConfig.username); factory.setPassword(RabbitConfig.password); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(RPC_QUEUE_NAME,false ,false ,false ,null ); channel.basicQos(1 ); QueueingConsumer consumer = new QueueingConsumer(channel); 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()); int n = Integer.parseInt(message); System.out.println(" [.] fib(" +message+")" ); String repsonse = "" +fib(n); channel.basicPublish("" , props.getReplyTo(), replyProps, repsonse.getBytes()); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false ); } } private static int fib (int n) throws Exception { if (n == 0 ) return 0 ; if (n == 1 ) return 1 ; return fib(n-1 ) + fib(n-2 ); } }
RPC客户端:
public class RPCClient { private Connection connection; private Channel channel; private String requestQueueName = "rpc_queue" ; private String replyQueueName; private QueueingConsumer consumer; public RPCClient () throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(RabbitConfig.ip); factory.setPort(RabbitConfig.port); factory.setUsername(RabbitConfig.username); factory.setPassword(RabbitConfig.password); connection = factory.newConnection(); channel = connection.createChannel(); replyQueueName = channel.queueDeclare().getQueue(); consumer = new QueueingConsumer(channel); channel.basicConsume(replyQueueName, true ,consumer); } public String call (String message) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException { String response = null ; String corrId = UUID.randomUUID().toString(); BasicProperties props = new BasicProperties.Builder() .correlationId(corrId) .replyTo(replyQueueName) .build(); channel.basicPublish("" , requestQueueName, props, message.getBytes()); while (true ){ QueueingConsumer.Delivery delivery = consumer.nextDelivery(); if (delivery.getProperties().getCorrelationId().equals(corrId)){ response = new String(delivery.getBody()); break ; } } return response; } public void close () throws Exception { connection.close(); } public static void main (String args[]) throws Exception { RPCClient fibRpc = new RPCClient(); System.out.println(" [x] Requesting fib(30)" ); String response = fibRpc.call("30" ); System.out.println(" [.] Got '" +response+"'" ); fibRpc.close(); } }
参考资料
Remote procedure call (RPC)
轻松搞定RabbitMQ(七)——远程过程调用RPC