1、远程过程调用(RPC)
在第二篇教程中我们介绍了如何使用工作队列(work queue)在多个工作者(woker)中间分发耗时的任务。
可是如果我们需要将一个函数运行在远程计算机上并且等待从那儿获取结果时,该怎么办呢?这就是另外的故事了。这种模式通常被称为远程过程调用(Remote Procedure Call)或者RPC。
这篇教程中,我们会使用RabbitMQ来构建一个RPC系统:包含一个客户端和一个RPC服务器。现在的情况是,我们没有一个值得被分发的足够耗时的任务,所以接下来,我们会创建一个模拟RPC服务来返回斐波那契数列。
2、客户端接口
为了展示RPC服务如何使用,我们创建了一个简单的客户端类。它会暴露出一个名为“call”的方法用来发送一个RPC请求,并且在收到回应前保持阻塞。代码如下:
1 FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient(); 2 String result = fibonacciRpc.call("4"); 3 System.out.println( "fib(4) is " + result);
关于RPC的注意事项:
尽管RPC在计算领域是一个常用模式,但它也经常被诟病。当一个问题被抛出的时候,程序员往往意识不到这到底是由本地调用还是由较慢的RPC调用引起的。同样的困惑还来自于系统的不可预测性和给调试工作带来的不必要的复杂性。跟软件精简不同的是,滥用RPC会导致不可维护的面条代码。
考虑到这一点,牢记以下建议:
确保能够明确的搞清楚哪个函数是本地调用的,哪个函数是远程调用的。给你的系统编写文档。保持各个组件间的依赖明确。处理错误案例。明了客户端改如何处理RPC服务器的宕机和长时间无响应情况。
当对避免使用RPC有疑问的时候。如果可以的话,你应该尽量使用异步管道来代替RPC类的阻塞。结果被异步地推送到下一个计算场景。
3、回调队列
一般来说通过RabbitMQ来实现RPC是很容易的。一个客户端发送请求信息,服务器端将其应用到一个回复信息中。为了接收到回复信息,客户端需要在发送请求的时候同时发送一个回调队列(callback queue)的地址。我们试试看:
1 callbackQueueName = channel.queueDeclare().getQueue(); 2 3 BasicProperties props = new BasicProperties 4 .Builder() 5 .replyTo(callbackQueueName) 6 .build(); 7 8 channel.basicPublish("", "rpc_queue", props, message.getBytes()); 9 10 // ... then code to read a response message from the callback_queue ...
消息属性:
delivery_mode(投递模式):将消息标记为持久的(值为2)或暂存的(除了2之外的其他任何值)。第二篇教程里接触过这个属性,记得吧?
content_type(内容类型):用来描述编码的mime-type。例如在实际使用中常常使用application/json来描述JOSN编码类型。
reply_to(回复目标):通常用来命名回调队列。
correlation_id(关联标识):用来将RPC的响应和请求关联起来。
4、关联标识
上边介绍的方法中,我们建议给每一个RPC请求新建一个回调队列。这不是一个高效的做法,幸好这儿有一个更好的办法 —— 我们可以为每个客户端只建立一个独立的回调队列。
这就带来一个新问题,当此队列接收到一个响应的时候它无法辨别出这个响应是属于哪个请求的。correlation_id 就是为了解决这个问题而来的。我们给每个请求设置一个独一无二的值。稍后,当我们从回调队列中接收到一个消息的时候,我们就可以查看这条属性从而将响应和请求匹配起来。如果我们接手到的消息的correlation_id是未知的,那就直接销毁掉它,因为它不属于我们的任何一条请求。
你也许会问,为什么我们接收到未知消息的时候不抛出一个错误,而是要将它忽略掉?这是为了解决服务器端有可能发生的竞争情况。尽管可能性不大,但RPC服务器还是有可能在已将应答发送给我们但还未将确认消息发送给请求的情况下死掉。如果这种情况发生,RPC在重启后会重新处理请求。这就是为什么我们必须在客户端优雅的处理重复响应,同时RPC也需要尽可能保持幂等性。
5、总结
RPC工作过程如下:
- 当客户端启动的时候,它创建一个匿名独享的回调队列。
- 在RPC请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。
- 将请求发送到一个 rpc_queue 队列中。
- RPC工作者(又名:服务器)等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给reply_to字段指定的队列。
- 客户端等待回调队列里的数据。当有消息出现的时候,它会检查correlation_id属性。如果此属性的值与请求匹配,将它返回给应用。
6、代码整合一起
斐波那契数列函数如下:
1 private static int fib(int n) { 2 if (n == 0) 3 return 0; 4 if (n == 1) 5 return 1; 6 return fib(n - 1) + fib(n - 2); 7 }
RPC 服务器代码RPCServer.java如下:
1 package rabbitmq.rpc; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 import com.rabbitmq.client.AMQP; 7 import com.rabbitmq.client.AMQP.BasicProperties; 8 import com.rabbitmq.client.Channel; 9 import com.rabbitmq.client.Connection; 10 import com.rabbitmq.client.Consumer; 11 import com.rabbitmq.client.DefaultConsumer; 12 import com.rabbitmq.client.Envelope; 13 14 import rabbitmq.utils.ConnectionUtils; 15 16 public class RPCServer { 17 //请求队列 18 private static final String RPC_QUEUE_NAME = "rpc_queue"; 19 20 public static void main(String[] args) { 21 Connection connection = null; 22 try { 23 connection = ConnectionUtils.getConnection(); 24 final Channel channel = connection.createChannel(); 25 channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null); 26 //采用公平调度模式 27 channel.basicQos(1); 28 Consumer consumer = new DefaultConsumer(channel) { 29 30 @Override 31 public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, 32 byte[] body) throws IOException { 33 AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder() 34 .correlationId(properties.getCorrelationId()).build(); 35 36 String response = ""; 37 38 try { 39 String message = new String(body, "UTF-8"); 40 int n = Integer.parseInt(message); 41 System.out.println(" [.] fib(" + message + ")"); 42 response += fib(n); 43 } catch (Exception e) { 44 System.out.println(" [.] " + e.toString()); 45 } finally { 46 //向回调队列发送结果信息 47 channel.basicPublish("", properties.getReplyTo(), replyProps, response.getBytes()); 48 //处理完后 发送消息确认 49 channel.basicAck(envelope.getDeliveryTag(), false); 50 synchronized (this) { 51 this.notify(); 52 } 53 } 54 } 55 }; 56 //采用应答模式监听,处理完后才从请求队列中删除请求 57 channel.basicConsume(RPC_QUEUE_NAME, false, consumer); 58 while (true) { 59 synchronized (consumer) { 60 try { 61 consumer.wait(); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 66 } 67 } 68 } catch (IOException | TimeoutException e) { 69 e.printStackTrace(); 70 } finally { 71 if (connection != null) { 72 try { 73 connection.close(); 74 } catch (IOException _ignore) { 75 } 76 } 77 } 78 } 79 80 private static int fib(int n) { 81 if (n == 0) 82 return 0; 83 if (n == 1) 84 return 1; 85 return fib(n - 1) + fib(n - 2); 86 } 87 }
RPC客户端的代码RPCClient.java如下:
1 package rabbitmq.rpc; 2 3 import static org.hamcrest.CoreMatchers.both; 4 5 import java.io.IOException; 6 import java.util.UUID; 7 import java.util.concurrent.ArrayBlockingQueue; 8 import java.util.concurrent.BlockingQueue; 9 import java.util.concurrent.TimeoutException; 10 11 import com.rabbitmq.client.AMQP; 12 import com.rabbitmq.client.AMQP.BasicProperties; 13 import com.rabbitmq.client.Channel; 14 import com.rabbitmq.client.Connection; 15 import com.rabbitmq.client.Consumer; 16 import com.rabbitmq.client.DefaultConsumer; 17 import com.rabbitmq.client.Envelope; 18 19 import rabbitmq.utils.ConnectionUtils; 20 21 public class RPCClient { 22 23 private Connection connection; 24 private Channel channel; 25 //请求队列 26 private static final String requestQueueName = "rpc_queue"; 27 //回调队列 28 private String replyQueueName; 29 public RPCClient() throws IOException, TimeoutException { 30 connection = ConnectionUtils.getConnection(); 31 channel = connection.createChannel(); 32 //随机生成一个回调队列 33 replyQueueName = channel.queueDeclare().getQueue(); 34 } 35 36 public String call(String message) throws IOException, InterruptedException { 37 //随机生产一个id 38 final String corrid = UUID.randomUUID().toString(); 39 AMQP.BasicProperties props = new AMQP.BasicProperties() 40 .builder() 41 .correlationId(corrid) 42 .replyTo(replyQueueName) 43 .build(); 44 channel.basicPublish("", requestQueueName, props, message.getBytes()); 45 final BlockingQueue<String> response = new ArrayBlockingQueue<>(1); 46 //声明一个消费者 用来消费回调队列 47 Consumer consumer = new DefaultConsumer(channel) { 48 49 @Override 50 public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) 51 throws IOException { 52 if(properties.getCorrelationId().equals(corrid)) { 53 response.offer(new String(body, "UTF-8")); 54 } 55 } 56 57 }; 58 //监听回调队列 59 channel.basicConsume(replyQueueName, true, consumer); 60 return response.take(); 61 } 62 63 public void close() throws IOException, TimeoutException { 64 channel.close(); 65 connection.close(); 66 } 67 }
main代码如下:
1 package rabbitmq.rpc; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 public class Main { 7 public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { 8 RPCClient fibonacciRpc = new RPCClient(); 9 10 System.out.println(" [x] Requesting fib(30)"); 11 String response = fibonacciRpc.call("30"); 12 System.out.println(" [.] Got '" + response + "'"); 13 14 fibonacciRpc.close(); 15 } 16 17 }
先启动RPCServer,再启动main方法,效果如下: