RabbitMQ之RPC实现
qyof1847
8年前
<h2>什么是RPC?</h2> <p>RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。</p> <p>为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用,</p> <p>RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。</p> <h2>RabbitMQ怎么实现RPC调用?</h2> <h3>Callback Queue</h3> <p>一般在RabbitMQ中做RPC是很简单的。客户端发送请求消息,服务器回复响应的消息。为了接受响应的消息,我们需要在请求消息中发送一个回调队列。可以使用默认的队列(which is exclusive in the java client.):</p> <pre> <code class="language-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...</code></pre> <h3>Message properties</h3> <p>AMQP协议为消息预定义了一组14个属性。</p> <pre> <code class="language-java">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;</code></pre> <p>大部分的属性是很少使用的。除了以下几种(其余有兴趣可以自行查看):</p> <ul> <li>deliveryMode: 标记消息传递模式,2-消息持久化,其他值-瞬态。</li> <li>contentType:内容类型,用于描述编码的mime-type. 例如经常为该属性设置JSON编码。</li> <li>replyTo:应答,通用的回调队列名称,</li> <li>correlationId:关联ID,方便RPC相应与请求关联。</li> </ul> <h3>Correlation Id</h3> <p>在上述方法中为每个RPC请求创建一个回调队列。这是很低效的。幸运的是,一个解决方案:可以为每个客户端创建一个单一的回调队列。</p> <p>新的问题被提出,队列收到一条回复消息,但是不清楚是那条请求的回复。这是就需要使用correlationId属性了。我们要为每个请求设置唯一的值。然后,在回调队列中获取消息,查看这个属性,关联response和request就是基于这个属性值的。如果我们看到一个未知的correlationId属性值的消息,可以放心的无视它——它不是我们发送的请求。</p> <p>你可能问道,为什么要忽略回调队列中未知的信息,而不是当作一个失败?这是由于在服务器端竞争条件的导致的。虽然不太可能,但是如果RPC服务器在发送给我们结果后,发送请求反馈前就挂掉了,这有可能会发送未知correlationId属性值的消息。如果发生了这种情况,重启RPC服务器将会重新处理该请求。这就是为什么在客户端必须很好的处理重复响应,RPC应该是幂等的。</p> <h3>Summary</h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/0406148a76ec904d6d949812a64637de.png"></p> <p>RPC的处理流程:</p> <ol> <li>当客户端启动时,创建一个匿名的回调队列。</li> <li>客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。</li> <li>请求被发送到rpc_queue队列中。</li> <li>RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。</li> <li>客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。</li> </ol> <h2>Demo Code</h2> <p>这里采用官网的一个例子来说明,RPC客户端通过RPC调用服务器来计算斐波那契额值。</p> <p>首先是服务端的代码:</p> <pre> <code class="language-java">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); } }</code></pre> <p>RPC客户端:</p> <pre> <code class="language-java">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(); } }</code></pre> <h2>参考资料</h2> <ol> <li><a href="/misc/goto?guid=4959737488896604846" rel="nofollow,noindex">Remote procedure call (RPC)</a></li> <li><a href="/misc/goto?guid=4959737488987796727" rel="nofollow,noindex">轻松搞定RabbitMQ(七)——远程过程调用RPC</a></li> </ol> <p> </p> <p>来自:http://blog.csdn.net/u013256816/article/details/55218595</p> <p> </p>