远程过程调用(RPC)

(使用Java客户端)

在指南的第二部分,我们学习了如何使用工作队列将耗时的任务分布到多个工作者中。

但是假如我们需要调用远端计算机的函数,等待结果呢?好吧,这又是另一个故事了。这模式通常被称为远程过程调用或RPC。

在这部分,我们将会使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们还没有值得分散的耗时任务,我们将会创建一个虚拟的RPC服务,用来返回Fibonacci(斐波纳契数列)。

用户接口

为了说明RPC服务如何使用,我们将会创建一个简单德客户端类。它会暴露一个叫call的方法,用来发送一个RPC请求,在响应回复之前都会一直阻塞:

  1. FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
  2. String result = fibonacciRpc.call("4");
  3. System.out.println( "fib(4) is " + result);

RPC方面的注意
虽然RPC在电脑运算方面是一个十分普通的模式,但是它依旧常常受批判的。
如果一个程序员没有意识到函数call是本地的还是一个迟钝的RPC。这结果是不可预知的很让你困惑的,并且会增加不必要的复杂调试。与简化软件相反,误用RPC会导致不可维护的意大利面条代码(译者注:原文是spaghetti code可能形容代码很长很乱)。

思想中煎熬,考虑下接下来的建议:
确保明显区分哪个是函数call是本地调用的,哪个是远端调用的。

给你的系统加上文档,让组件之间的依赖项清晰可见的。

处理错误事件。当RPC服务器很久没有响应了,客户端应该如何响应?
当关于RPC的所有疑问消除,在你可以的情况下,你应该使用一个异步的管道,代替RPC中阻塞,结果会异步的放入接下来的计算平台。

回收队列

一般来说在RabbitMQ上做RPC是容易的。一个客户端发送一个请求消息,一个服务器返回响应消息。为了接受到响应,我们需要再请求中带上一个callback队列的地址。我们可以使用默认队列(那个在Java客户端上市独占的)。让我们试一下:

  1. callbackQueueName = channel.queueDeclare().getQueue();
  2. BasicProperties props = new BasicProperties
  3. .Builder()
  4. .replyTo(callbackQueueName)
  5. .build();
  6. channel.basicPublish("", "rpc_queue", props, message.getBytes());
  7. // ... then code to read a response message from the callback_queue ...

消息属性
这AMQP协议预先确定了消息中的14个属性。他们大多数属性很少使用,除了下面这些例外:
deliveryMode:将一个消息标记为持久化(值为2)或者瞬态的(其他值)。你可能从第二部分中记起这个属性。
contentType:用来描述媒体类型的编码。例如常常使用的JSON编码,这是一个好的惯例,设置这个属性为:application/json
replyTo:通常来命名回收队列的名字。
correlationId:对RPC加速响应请求是很有用的。

我们需要这个新的引用:

  1. import com.rabbitmq.client.AMQP.BasicProperties;

相关性ID (原:Correlation Id)

在当前方法中我们建议为每一个RPC请求创建一个回收队列。这个效率十分低下的,但幸运的是有一个更好的方式- 让我们为每一个客户端创建一个单一的回收队列。
这样又出现了新的问题,没有清晰的判断队列中的响应是属于哪个请求的。这个时候coorrelationId属性发挥了作用。我们将每个请求的这个属性设置为唯一值。以后当我们在回收队列中接收消息时,我们将会查看这个属性,依据这个属性值,我们是能将每个响应匹配的对应的请求上。如果我们遇见个未知的correlationId值,我们可以安全的丢弃这个消息-因为它不属于任何一个我们的请求。

你可能会问,为什么我们要忽略哪些在回收队列中未知的消息,而不是以一个错误结束?因为在服务器竟态条件下,这种情况是可能的。RPC服务器发送给
我们答应之后,在发送一个确认消息之前,就死掉了,虽然这种可能性不大,但是它依旧存在可能。如果这事情发生了,RPC服务器重启之后,将会再一次处理请
求。这就是为什么我们要温和地处理重复的响应,这RPC理想情况下是幂等的。

摘要


我们的RPC将会像这样工作:

当客户端启动,它会创建一个匿名的独占的回收队列。
对于一个RPC请求,客户端会发送一个消息中有两个属性:replyTo,要发送的的回收队列和correlationId,对于每一个请求都是唯一值。
这请求发送到rpc_queue队列中。
这RPC工作者(亦称:服务器)等候队列中的请求。当请求出现,它处理这工作并发送携带结果的信息到客户端,使用的队列是消息属性replTo中的那个。
客户端等待回收队列中的数据。当一个消息出现,它会检查correlationId属性。如果它符合请求中的值,它会返回这响应给应用程序。

把所有的放在一起

斐波那契任务:

  1. private static int fib(int n) throws Exception {
  2. if (n == 0) return 0;
  3. if (n == 1) return 1;
  4. return fib(n-1) + fib(n-2);
  5. }

我们声明我们的斐波那契函数。它假定一个合法的正整数做为输入参数。(不要期望这个可以处理大量数字,它可能是最慢的递归实现了)。
我们的RPC服务器RPCServer.java的代码:

  1. private static final String RPC_QUEUE_NAME = "rpc_queue";
  2. ConnectionFactory factory = new ConnectionFactory();
  3. factory.setHost("localhost");
  4. Connection connection = factory.newConnection();
  5. Channel channel = connection.createChannel();
  6. channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
  7. channel.basicQos(1);
  8. QueueingConsumer consumer = new QueueingConsumer(channel);
  9. channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
  10. System.out.println(" [x] Awaiting RPC requests");
  11. while (true) {
  12. QueueingConsumer.Delivery delivery = consumer.nextDelivery();
  13. BasicProperties props = delivery.getProperties();
  14. BasicProperties replyProps = new BasicProperties
  15. .Builder()
  16. .correlationId(props.getCorrelationId())
  17. .build();
  18. String message = new String(delivery.getBody());
  19. int n = Integer.parseInt(message);
  20. System.out.println(" [.] fib(" + message + ")");
  21. String response = "" + fib(n);
  22. channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes());
  23. channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  24. }

这服务器代码是相当简单明了的:
如往常一样,我们开始建立连接,通道和声明队列。
我们可能想运行不止一个服务器进程。为了均衡的负载到多个服务器上,我们需要设置channel.basicQos中的prefetchCount属性。
我们使用basicConsume访问队列。然后进入while循环,我们等待请求消息,处理工作,发送响应。

我们RPC客户端RPCClient.java的代码:

  1. private Connection connection;
  2. private Channel channel;
  3. private String requestQueueName = "rpc_queue";
  4. private String replyQueueName;
  5. private QueueingConsumer consumer;
  6. public RPCClient() throws Exception {
  7. ConnectionFactory factory = new ConnectionFactory();
  8. factory.setHost("localhost");
  9. connection = factory.newConnection();
  10. channel = connection.createChannel();
  11. replyQueueName = channel.queueDeclare().getQueue();
  12. consumer = new QueueingConsumer(channel);
  13. channel.basicConsume(replyQueueName, true, consumer);
  14. }
  15. public String call(String message) throws Exception {
  16. String response = null;
  17. String corrId = java.util.UUID.randomUUID().toString();
  18. BasicProperties props = new BasicProperties
  19. .Builder()
  20. .correlationId(corrId)
  21. .replyTo(replyQueueName)
  22. .build();
  23. channel.basicPublish("", requestQueueName, props, message.getBytes());
  24. while (true) {
  25. QueueingConsumer.Delivery delivery =consumer.nextDelivery();
  26. if (delivery.getProperties().getCorrelationId().equals(corrId)) {
  27. response = new String(delivery.getBody());
  28. break;
  29. }
  30. }
  31. return response;
  32. }
  33. public void close() throws Exception {
  34. connection.close();
  35. }

The client code is slightly more involved:
这客户端代码是更加清晰:
我们建立一个连接和通道并且声明一个独占的callback队列用来等待答复。
我们订阅这个callback队列,以便于我们可以接收到RPC响应。
我们的call方法做这真正的RPC请求。
接着,我们首次生成一个唯一的correlationId数字并且保存它,在循环中使用这个值找到合适的响应。
接下来,我们发布请求消息,带着两个属性:replyTocorrelationId
这时候,我们可以坐下来,等着合适的响应抵达。
这循环中做了个简单德工作,检查每一个响应消息中correlationId值,是否是它要寻找的。如果是,它会保存这响应。
最终,我们把响应返回给用户。

制造客户端请求:

  1. RPCClient fibonacciRpc = new RPCClient();
  2. System.out.println(" [x] Requesting fib(30)");
  3. String response = fibonacciRpc.call("30");
  4. System.out.println(" [.] Got '" + response + "'");
  5. fibonacciRpc.close();

现在是时候让我们回顾下我们RPCClient.javaRPCServer.java中的全部例子的源码(包含基本的异常处理)。
编译和如往常一样建立类路径(看指南的第一部分)

  1. $ javac -cp rabbitmq-client.jar RPCClient.java RPCServer.java

我们的RPC服务现在准备好了,我们启动着服务器:

  1. $ java -cp $CP RPCServer
  2. [x] Awaiting RPC requests

为了请求一个斐波那契数字,运行客户端:

  1. $ java -cp $CP RPCClient
  2. [x] Requesting fib(30)

现在的设计不仅仅可以实现一个RPC服务,并且它还有几项重要的优势:
如果RPC服务器反应太迟缓,你可以通过运行另一个程序来扩展。试着通过一个新的控制平台来运行第二个RPC服务器。在客户端这边,RPC要求仅发送和接收一个消息。像queueDeclare非同步调用是被要求的。因此,RPC客户端仅仅需要一个网络循环的单一RPC请求。

我们的代码一直是十分简单的,不能试着解决更复杂(但是重要)的问题,比如:
如果没有服务器运行,客户端如何响应?
客户端是否对RPC的超时有处理?
如果服务器发生故障,抛出一个异常,是否应该传递到客户端?
在处理之前把进入来的非法消息隔离掉(检查界限,类型)。

转载RabbitMQ入门(6)--远程调用的更多相关文章

  1. 转载RabbitMQ入门(3)--发布和订阅

    发布和订阅 (使用java 客户端) 在先前的指南中,我们创建了一个工作队列.这工作队列后面的假想是每一个任务都被准确的传递给工作者.在这部分我们将会做一些完全不同的事情–我们将一个消息传递给多个消费 ...

  2. 转载RabbitMQ入门(5)--主题

    主题(topic) (使用Java客户端) 在先前的指南中我们改进了我们的日志系统.取代使用fanout类型的交易所,那个仅仅有能力实现哑的广播,我们使用一个direct类型的交易所,获得一个可以有选 ...

  3. 转载RabbitMQ入门(2)--工作队列

    工作队列 (使用Java客户端) 在这第一指南部分,我们写了通过同一命名的队列发送和接受消息.在这一部分,我们将会创建一个工作队列,在多个工作者之间使用分布式时间任务. 工作队列(亦称:任务队列)背后 ...

  4. 转载RabbitMQ入门(1)--介绍

    目录[-] "Hello World" (使用java客户端) 发送 接收 把所有放在一起 前面声明本文都是RabbitMQ的官方指南翻译过来的,由于本人水平有限难免有翻译不当的地 ...

  5. 转载RabbitMQ入门(4)--路由

    路由 (使用Java客户端) 在先前的指南中,我们建立了一个简单的日志系统.我们可以将我们的日志信息广播到多个接收者. 在这部分的指南中,我们将要往其中添加一个功能-让仅仅订阅一个消息的子集成为可能. ...

  6. RabbitMQ消息队列(七):适用于云计算集群的远程调用(RPC)

            在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成.那么,RabbitMQ如何使用RPC呢?在本篇 ...

  7. RabbitMQ 适用于云计算集群的远程调用(RPC)

    在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成.那么,RabbitMQ如何使用RPC呢?在本篇文章中,我们将会 ...

  8. (转)RabbitMQ消息队列(七):适用于云计算集群的远程调用(RPC)

    在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成.那么,RabbitMQ如何使用RPC呢?在本篇文章中,我们将会 ...

  9. RabbitMQ入门教程 [转]

    1.引言 RabbitMQ--Rabbit Message Queue的简写,但不能仅仅理解其为消息队列,消息代理更合适.消息队列主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩 ...

随机推荐

  1. C#textbox右击弹出菜单

    给窗口体拖一个contextMenuTrip 控件,也就是右键菜单控件,这时你就不要给这个控件写内容了, 选中textBox 然后点属性窗口,把它的contextMenuTrip 属性选中你刚才托的那 ...

  2. PAT-乙级-1046. 划拳(15)

    1046. 划拳(15) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHEN, Yue 划拳是古老中国酒文化的一个有趣的组成部分 ...

  3. win8 任务栏不合并隐藏标题

    让win8任务栏不合并,并且隐藏标题的办法: 效果如下: 首先让win8不合并任务栏 1.任务栏上点鼠标右键 -- "属性" 2."任务栏按钮"选择" ...

  4. hdu 1166 树状数组 线段树入门

    点修改 区间求和 #include <cstdio> #include <cstdlib> #include <cmath> #include <map> ...

  5. 【C++之STL】理解容器(ing)

    “容器可容纳一些数据的模板类” “容器是包容其他对象的对象” 两种类型:顺序容器.关联容器   顺序容器 关联容器 访问成员 顺序访问和随机访问 经过优化关键键值访问                 ...

  6. Searching a 2D Sorted Matrix Part I

    Write an efficient algorithm that searches for a value in an n x m table (two-dimensional array). Th ...

  7. JsRender系列demo(6)-无名

    <!DOCTYPE html> <html> <head> <script src="http://code.jquery.com/jquery.j ...

  8. 关于dynamic_cast

    http://www.groad.net/bbs/read.php?tid-5476.html dynamic_cast 进行运行时强制转换.如果在执行时转换非法,则会转换失败.dynamic_cas ...

  9. 李洪强iOS开发之keychain的使用

    通常情况下,我们用NSUserDefaults存储数据信息,但是对于一些私密信息,比如密码.证书等等,就需要使用更为安全的keychain了.keychain里保存的信息不会因App被删除而丢失,在用 ...

  10. SDK 与MFC

    SDK 就是Software Development Kit 软件开发包MFC 就是Microsoft Foundation Classes 微软函数类库.是以C++类的形式封装了Windows的AP ...