1、远程过程调用(RPC)

  在第二篇教程中我们介绍了如何使用工作队列(work queue)在多个工作者(woker)中间分发耗时的任务。

  可是如果我们需要将一个函数运行在远程计算机上并且等待从那儿获取结果时,该怎么办呢?这就是另外的故事了。这种模式通常被称为远程过程调用(Remote Procedure Call)或者RPC。

  这篇教程中,我们会使用RabbitMQ来构建一个RPC系统:包含一个客户端和一个RPC服务器。现在的情况是,我们没有一个值得被分发的足够耗时的任务,所以接下来,我们会创建一个模拟RPC服务来返回斐波那契数列。

2、客户端接口

  为了展示RPC服务如何使用,我们创建了一个简单的客户端类。它会暴露出一个名为“call”的方法用来发送一个RPC请求,并且在收到回应前保持阻塞。代码如下:

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

关于RPC的注意事项:

  尽管RPC在计算领域是一个常用模式,但它也经常被诟病。当一个问题被抛出的时候,程序员往往意识不到这到底是由本地调用还是由较慢的RPC调用引起的。同样的困惑还来自于系统的不可预测性和给调试工作带来的不必要的复杂性。跟软件精简不同的是,滥用RPC会导致不可维护的面条代码。

  考虑到这一点,牢记以下建议:

  确保能够明确的搞清楚哪个函数是本地调用的,哪个函数是远程调用的。给你的系统编写文档。保持各个组件间的依赖明确。处理错误案例。明了客户端改如何处理RPC服务器的宕机和长时间无响应情况。

  当对避免使用RPC有疑问的时候。如果可以的话,你应该尽量使用异步管道来代替RPC类的阻塞。结果被异步地推送到下一个计算场景。

3、回调队列

  一般来说通过RabbitMQ来实现RPC是很容易的。一个客户端发送请求信息,服务器端将其应用到一个回复信息中。为了接收到回复信息,客户端需要在发送请求的时候同时发送一个回调队列(callback queue)的地址。我们试试看:

 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 ...

消息属性:

  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、代码整合一起

  斐波那契数列函数如下:

 private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}

  RPC 服务器代码RPCServer.java如下:

 package rabbitmq.rpc;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; 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.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class RPCServer {
//请求队列
private static final String RPC_QUEUE_NAME = "rpc_queue"; public static void main(String[] args) {
Connection connection = null;
try {
connection = ConnectionUtils.getConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
//采用公平调度模式
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
.correlationId(properties.getCorrelationId()).build(); String response = ""; try {
String message = new String(body, "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
response += fib(n);
} catch (Exception e) {
System.out.println(" [.] " + e.toString());
} finally {
//向回调队列发送结果信息
channel.basicPublish("", properties.getReplyTo(), replyProps, response.getBytes());
//处理完后 发送消息确认
channel.basicAck(envelope.getDeliveryTag(), false);
synchronized (this) {
this.notify();
}
}
}
};
//采用应答模式监听,处理完后才从请求队列中删除请求
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
while (true) {
synchronized (consumer) {
try {
consumer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (IOException _ignore) {
}
}
}
} private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
}

  RPC客户端的代码RPCClient.java如下:

 package rabbitmq.rpc;

 import static org.hamcrest.CoreMatchers.both;

 import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException; 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.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class RPCClient { private Connection connection;
private Channel channel;
//请求队列
private static final String requestQueueName = "rpc_queue";
//回调队列
private String replyQueueName;
public RPCClient() throws IOException, TimeoutException {
connection = ConnectionUtils.getConnection();
channel = connection.createChannel();
//随机生成一个回调队列
replyQueueName = channel.queueDeclare().getQueue();
} public String call(String message) throws IOException, InterruptedException {
//随机生产一个id
final String corrid = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties()
.builder()
.correlationId(corrid)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, props, message.getBytes());
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
//声明一个消费者 用来消费回调队列
Consumer consumer = new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
if(properties.getCorrelationId().equals(corrid)) {
response.offer(new String(body, "UTF-8"));
}
} };
//监听回调队列
channel.basicConsume(replyQueueName, true, consumer);
return response.take();
} public void close() throws IOException, TimeoutException {
channel.close();
connection.close();
}
}

  main代码如下:

 package rabbitmq.rpc;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; public class Main {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
RPCClient fibonacciRpc = new RPCClient(); System.out.println(" [x] Requesting fib(30)");
String response = fibonacciRpc.call("30");
System.out.println(" [.] Got '" + response + "'"); fibonacciRpc.close();
} }

  先启动RPCServer,再启动main方法,效果如下:

  

rabbitmq系列五 之远程过程调用(RPC)的更多相关文章

  1. Python操作rabbitmq系列(六):进行RPC调用

    此刻,我们已经进入第6章,是官方的最后一个环节,但是,并非本系列的最后一个环节.因为在实战中还有一些经验教训,并没体现出来.由于马上要给同事没培训celery了.我也来不及写太多.等后面,我们再慢慢补 ...

  2. [译]RabbitMQ教程C#版 - 远程过程调用(RPC)

    先决条件 本教程假定 RabbitMQ 已经安装,并运行在localhost标准端口(5672).如果你使用不同的主机.端口或证书,则需要调整连接设置. 从哪里获得帮助 如果您在阅读本教程时遇到困难, ...

  3. RabbitMQ系列(五)--高级特性

    在上一篇文章讲解MQ消息可靠性投递和幂等性中有提到confirm机制的重要性,现在更相信的说明一下 一.Confirm机制 Confirm就是消息确认,当Producer发送消息,如果Broker收到 ...

  4. rabbitmq系列五 之主题交换机

    1.主题 在前面的例子中,我们对日志系统进行了改进.使用了direct交换机代替了fanout交换机,从只能盲目的广播消息改进为有可能选择性的接收日志. 尽管直接交换机能够改善我们的日志系统,但是它也 ...

  5. RabbitMQ系列教程之六:远程过程调用(RPC)(转载)

    RabbitMQ系列教程之六:远程过程调用(RPC) 远程过程调用(Remote Proceddure call[RPC]) (本实例都是使用的Net的客户端,使用C#编写) 在第二个教程中,我们学习 ...

  6. RabbitMQ入门:远程过程调用(RPC)

    假如我们想要调用远程的一个方法或函数并等待执行结果,也就是我们通常说的远程过程调用(Remote Procedure Call).怎么办? 今天我们就用RabbitMQ来实现一个简单的RPC系统:客户 ...

  7. RabbitMQ系列(三)RabbitMQ交换器Exchange介绍与实践

    RabbitMQ交换器Exchange介绍与实践 RabbitMQ系列文章 RabbitMQ在Ubuntu上的环境搭建 深入了解RabbitMQ工作原理及简单使用 RabbitMQ交换器Exchang ...

  8. RabbitMQ系列(二)深入了解RabbitMQ工作原理及简单使用

    深入了解RabbitMQ工作原理及简单使用 RabbitMQ系列文章 RabbitMQ在Ubuntu上的环境搭建 深入了解RabbitMQ工作原理及简单使用 RabbitMQ交换器Exchange介绍 ...

  9. RabbitMQ系列教程之一:我们从最简单的事情开始!Hello World(转载)

    RabbitMQ系列教程之一:我们从最简单的事情开始!Hello World 一.简介 RabbitMQ是一个消息的代理器,用于接收和发送消息,你可以这样想,他就是一个邮局,当您把需要寄送的邮件投递到 ...

随机推荐

  1. hdu-1394(线段树&逆序数的性质和求法)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1394 题目大意: 给出一个序列,一对逆序数就是满足i<j&&a[i]>a[ ...

  2. hdu-1036(格式题+精确度)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1036 心得:注意,要进行四舍五入的精确可以用+0.5实现. #include<iostream& ...

  3. Java基本数据类型与包装类、字符串之间的转换

    基本数据类型和包装类的转换:1.装箱:基本数据类型→对应包装类,可分为手动装箱和自动装箱.2.拆箱:包装类→对应基本数据类型,可分为手动拆箱和自动拆箱. 例子:手动装箱:Integer iObj=ne ...

  4. java常用设计模式二:工厂模式

    1.简单工厂模式(静态工厂方法模式) 抽象实例: public interface People { void talk(); } 具体实例: public class Doctor implemen ...

  5. 清理docker大日志文件

    1.进入容器文件的存放目录 ,并查看某一个容器的文件大小 [root@auto ~]# [root@auto ~]# cd /var/lib/docker/containers [root@auto ...

  6. 详细解读 :java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed,Java报错之Connection is read-only.

    问题分析: 实际开发项目中,进行insert的时候,产生这个问题是Spring框架的一个安全权限保护方法,对于方法调用的事物保护,一般配置如下: <!-- 事务管理 属性 --> < ...

  7. es6函数的新语法

    函数的默认值 function(x,y=10){ //赋予了y就有了默认值为10,如果y赋值了,就回被覆盖,否则就是默认值10 变量已经有默认值了,所以不用再次用let或者const声明啦 }

  8. DDR中寄存器的问题

    图中虚线是自动跳转,实线是通过发送命令才能跳转的. 下面是框中对应的命令. ACT = ACTIVATE MPR = Multipurpose register MRS = Mode register ...

  9. js动态删除某一行,内容超出单元格后超出的部分用省略号代替

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <s ...

  10. 一份不太简短的LaTeX模板

    编译环境: Ubuntu16.04 texllive2016 sublime text3 + latextools 该模板使用与自己写文档,记笔记,记录代码,写作业等等. %!TEX program ...