远程过程调用(RPC)

在前面我们已经学习了如何使用工作队列在多个消费者之间分配耗时的任务。

但是如果我们需要在远程计算机上运行功能并等待结果怎么办?那将会是一个不同的故事。此模式通常称为远程过程调用或RPC

在本教程中,我们将使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回斐波纳契数字的虚拟RPC服务。

客户端界面

为了说明如何使用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的阻塞,结果被异步推送到下一个计算阶段。

回调队列

一般来说RPC在RabbitMQ上很容易。客户端发送请求消息(客户端发送请求时充当生产者,接收响应时又为消费者),服务器回复响应消息(服务器返回响应充当生产者,接收请求充当消费者)。为了收到响应,我们需要发送一个'回调'队列地址与请求。我们可以使用默认队列(在Java客户端中是排他性的)。我们来试试吧

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
.Builder()
.replyTo(callbackQueueName)
.build(); channel.basicPublish(“”,“rpc_queue”,props,message.getBytes()); // ...然后代码从callback_queue读取一个响应消息...

消息属性

AMQP 0-9-1协议为消息预定义了一组含有14个属性的集合。大多数属性很少使用,除了以下内容:

  • deliveryMode:将消息标记为持久性(值为2)或transient(任何其他值)。
  • contentType:用于描述mime类型的编码。例如对于经常使用的JSON编码,将此属性设置为:application / json是一个很好的做法。
  • replyTo:通常用来命名一个回调队列。
  • correlationId:用于将RPC响应与请求相关联。

我们需要这个新的导入:

import com.rabbitmq.client.AMQP.BasicProperties;

关联标识

在上面提出的方法中,我们建议为每个RPC请求创建一个回调队列。这是非常低效的,但幸运的是有一个更好的方法 - 让我们为每个客户端创建一个回调队列。

这引发了一个新的问题,在该队列中收到响应,响应所属的请求不清楚。那么可以使用correlationId属性区分。我们将为每个请求设置唯一的值。之后,当我们在回调队列中收到一条消息时,我们将查看此属性,并且基于此,我们将能够将响应与请求相匹配。如果我们看到一个未知的 correlationId值,我们可能会安全地丢弃该消息 - 它不属于我们的请求。

您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是出现错误?这是由于在服务器端发生竞争条件的可能性。尽管不太可能,RPC服务器可能会在发送给我们的回应之后,却在发送请求的确认消息之前down机。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端上,我们必须优雅地处理这些重复的响应,而RPC应该理想地是幂等的。

概要

我们的RPC将像这样工作:

  • 当客户端启动时,它创建一个匿名独占回调队列。
  • 对于RPC请求,客户端发送一个具有两个属性的消息: replyTo,它被设置为回调队列和correlationId,它被设置为每个请求的唯一值。
  • 请求被发送到rpc_queue队列。
  • RPC worker(aka:server)正在等待队列上的请求。当请求出现时,它将执行该作业,并使用replyTo字段中的队列将结果发送回客户端。
  • 客户端等待回调队列中的数据。当信息出现时,它检查correlationId属性。如果它与请求中的值相匹配,则返回对应用程序的响应。

完整示例

RPCServer.java

 package com.rabbitMQ;

 import java.io.IOException;

 import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; public class RPCServer { private static final String RPC_QUEUE_NAME = "rpc_queue"; private static int fib(int n) {
if (n == ) return ;
if (n == ) return ;
return fib(n-) + fib(n-);
} public static void main(String[] argv) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); Connection connection = null;
try {
connection = factory.newConnection();
Channel channel = connection.createChannel(); channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null); channel.basicQos(); System.out.println(" [x] Awaiting RPC requests"); Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.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 (RuntimeException e){
System.out.println(" [.] " + e.toString());
}
finally {
channel.basicPublish( "", properties.getReplyTo(), replyProps, response.getBytes("UTF-8")); channel.basicAck(envelope.getDeliveryTag(), false);
}
}
}; channel.basicConsume(RPC_QUEUE_NAME, false, consumer); } catch(Exception e) {
e.printStackTrace(); }
} }

服务器代码相当简单:

  • 像往常一样,我们开始建立连接,通道和声明队列。
  • 我们可能想要运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要在channel.basicQos中设置prefetchCount设置。
  • 我们使用basicConsume访问请求队列,我们​​以对象(DefaultConsumer)的形式提供一个回调,该对象将执行该操作并发回响应。

RPCClient.java

 package com.rabbitMQ;

 import com.rabbitmq.client.*;

 import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException; public class RPCClient { private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName; public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); connection = factory.newConnection();
channel = connection.createChannel(); replyQueueName = channel.queueDeclare().getQueue();
} public String call(String message) throws IOException, InterruptedException {
String corrId = UUID.randomUUID().toString(); AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
//发布请求到replyQueueName
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8")); final BlockingQueue<String> response = new ArrayBlockingQueue<String>();
//从replyQueueName获取响应
channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
if (properties.getCorrelationId().equals(corrId)) {
response.offer(new String(body, "UTF-8"));
}
}
}); return response.take();
} public void close() throws IOException {
connection.close();
} public static void main(String[] args) throws Exception { RPCClient fibonacciRpc = new RPCClient(); System.out.println("[x] Requesting fib(30)");
String response = fibonacciRpc.call("");
System.out.println("[.] Got'" + response + "'"); fibonacciRpc.close(); }
}

客户端代码涉及的东西稍微多一点:

  • 我们建立一个连接和通道,并声明一个独占的'回拨'队列作为回复。
  • 我们订阅“回调”队列,以便我们可以接收RPC响应。
  • 我们调用call方法发出RPC请求。
  • 在这里,我们首先生成一个唯一的correlationId 数字并保存它 - 我们在DefaultConsumer 中实现handleDelivery将使用此值来捕获与correlationID匹配的响应。
  • 接下来,我们发布请求消息,其中包含两个属性: replyTo和correlationId。
  • 在这一点上,我们可以坐下来等待适当的响应到达。
  • 由于我们的消费者处理发生在一个单独的线程中,我们将需要一些东西在响应到达之前暂停主线程。我们这里使用了BlockingQueue阻塞队列。这里我们创建了一个ArrayBlockingQueue阻塞队列,容量设置为1,因为我们只需要等待一个响应。
  • 该handleDelivery方法是做一个很简单的工作,对每一位消费响应消息它会检查的correlationID 是否是我们要的响应。如果是,就把响应内容放到阻塞队列中。
  • 同时主线程正在等待响应从BlockingQueue中取出。
  • 最后,我们将响应返回给用户。

使客户端请求:

RPCClient fibonacciRpc = new RPCClient();

System.out.println(“[x] Requesting fib(30)”);
String response = fibonacciRpc.call(“30”);
System.out.println(“[.] Got'” + response + “'”); fibonacciRpc.close();

rabbitMQ_rpc(六)的更多相关文章

  1. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

  2. MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息

    MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二 ...

  3. 【原】AFNetworking源码阅读(六)

    [原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...

  4. CRL快速开发框架系列教程六(分布式缓存解决方案)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  5. 【微信小程序开发•系列文章六】生命周期和路由

    这篇文章理论的知识比较多一些,都是个人观点,描述有失妥当的地方希望读者指出. [微信小程序开发•系列文章一]入门 [微信小程序开发•系列文章二]视图层 [微信小程序开发•系列文章三]数据层 [微信小程 ...

  6. 我的MYSQL学习心得(六) 函数

    我的MYSQL学习心得(六) 函数 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) 数据类 ...

  7. 我的MYSQL学习心得(十六) 优化

    我的MYSQL学习心得(十六) 优化 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) 数据 ...

  8. ASP.NET MVC Model绑定(六)

    ASP.NET MVC Model绑定(六) 前言 前面的篇幅对于IValueProvider的使用做个基础的示例讲解,但是没并没有对 IValueProvider类型的实现做详细的介绍,然而MVC框 ...

  9. redis成长之路——(六)

    redis配置 为了码农在代码上只关心业务以及代码上的统一性,wenli.drive.redis内部使用配置来完成那些不同的场景,也就是说随便填填配置就能适应不同的场景! 当然配置多了码农也会受不了, ...

随机推荐

  1. ZooKeeper学习第八期——ZooKeeper伸缩性(转)

    转载来源:https://www.cnblogs.com/sunddenly/p/4143306.html 一.ZooKeeper中Observer 1.1 ZooKeeper角色 经过前面的介绍,我 ...

  2. AWR报告分析案例及命令(收集)

    AWR报告分析案例(收集) 循序渐进解读Oracle AWR性能分析报告 AWR报告分析之一:高 DB CPU 消耗的性能根源 生成AWR报告命令: 1)连接数据库:sqlplus / as sysd ...

  3. 推荐一个Redis管理工具

    Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理.它支持字符串.哈希表.列表.集合.有序集合,位图,hyperloglogs等数据类型.内置复制.Lu ...

  4. Hadoop —— 单机环境搭建

    一.前置条件 Hadoop的运行依赖JDK,需要预先安装,安装步骤见: Linux下JDK的安装 二.配置免密登录 Hadoop组件之间需要基于SSH进行通讯. 2.1 配置映射 配置ip地址和主机名 ...

  5. spring 5.x 系列第14篇 —— 整合RabbitMQ (代码配置方式)

    源码Gitub地址:https://github.com/heibaiying/spring-samples-for-all 一.说明 1.1 项目结构说明 本用例关于rabbitmq的整合提供简单消 ...

  6. Liunx 安装 Nessus

    Liunx 安装 Nessus   啥子是Nessus 它是一款系统漏洞扫描与分析软件,可以扫描服务器存在哪些漏洞,页面简介美观,非常Nice. 获取激活码 首先访问如下网站 https://www. ...

  7. 与 MySQL 因“CST” 时区协商误解导致时间差了13 小时

    CST 时区名为 CST 的时区是一个很混乱的时区,有四种含义: 美国中部时间 Central Standard Time (USA) UTC-05:00 / UTC-06:00 澳大利亚中部时间 C ...

  8. fis3前端工程构建配置入门教程

    一.前言 fis3是百度推出的一款前端工程构建工具,类似的还有webpack,gulp等工具:无论大家有没有使用过,从事前端行业应该都略知一二了,所以对于此类工具用干嘛的我这里就不做重复了. 其实对于 ...

  9. 不懂数据库索引的底层原理?那是因为你心里没点b树

    本文在个人技术博客不同步发布,详情可用力戳 亦可扫描屏幕右侧二维码关注个人公众号,公众号内有个人联系方式,等你来撩...   前几天下班回到家后正在处理一个白天没解决的bug,厕所突然传来对象的声音: ...

  10. 鼠标滑至某位置,在鼠标旁边出现详情弹窗div

    首先效果如下: 代码如下: //这个是一个循环,循环所有name为xx的td标签(也就是给tdname为XXX的添加事件)$("td[name='strGoodsSKU']").e ...