远程过程调用(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. Hexo+NexT(二):Hexo站点配置详解

    阅读本篇之前,假定读者已经有了Node.js的基础,如需要补充Node.js知识的,请自行百度. Hexo是在Node.js框架下的一个项目,利用Node.js提供的强大功能,完成从Markdown到 ...

  2. ASP.NET MVC/Core表单提交后台模型二级属性验证问题

    起因 这个是网友在官网论坛的提问:https://fineui.com/bbs/forum.php?mod=viewthread&tid=22237 重新问题 本着务实求真的态度,我们先来复现 ...

  3. MCtalk对话抱抱星英语:从Diss在线英语教学乱象到回归教育本原

    在百度搜索输入“在线英语”四字,清一色的线上英语培训品牌琳琅满目,“免费英语学习”.“外教口语一对一培训”.“四级听力”.“专属外教”,竞争之激烈可见一斑,创业公司绞尽脑汁挖掘细分市场,试图在一片红海 ...

  4. Hive —— 安装部署

    一.安装Hive 1.1 下载并解压 下载所需版本的Hive,这里我下载版本为cdh5.15.2.下载地址:http://archive.cloudera.com/cdh5/cdh/5/ # 下载后进 ...

  5. git push 时:报missing Change-Id in commit message footer的错误

    1. 一般而言,按照提示执行以下两个命令即可生成新的Change-id - gitdir=$(git rev-parse --git-dir); scp -p -P 29418 guan@192.16 ...

  6. PATB 1038. 统计同成绩学生(20)

    https://www.patest.cn/contests/pat-b-practise/1038 #include <cstdio> int cnt[110]; int temp[10 ...

  7. Docker镜像和容器管理(二)

    Docker安装 Docker镜像管理 https://hub.docker.com/ 是公共的一个Docker镜像仓库,类似GitHub一样,上面有非常多的开源项目镜像. 可以直接在命令行搜索镜像 ...

  8. 透视BlueStore存储结构:如何根据文件名从裸盘提取文件内容

    在FileStore下,用户文件经过切分对象块后最终存放在了单机文件系统(xfs .ext4等)中,我们可以较容易地找到这些对象块对应的文件,然后提取这些对象块文件后组装成用户文件.然而,BlueSt ...

  9. C++学习书籍推荐《C++编程思想第二版第二卷》下载

    百度云及其他网盘下载地址:点我 编辑推荐 “经典原版书库”是响应教育部提出的使用原版国外教材的号召,为国内高校的计算机教学度身订造的.<C++编程思想>(英文版第2版)是书库中的一本,在广 ...

  10. bzoj1584 9.20考试 cleaning up 打扫卫生

    1584: [Usaco2009 Mar]Cleaning Up 打扫卫生 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 549  Solved: 38 ...