一次RPC调用过程是怎么样的?

注册中心

RPC(Remote Procedure Call)翻译成中文就是 {远程过程调用}。RPC 框架起到的作用就是为了实现,调用远程方法时,能够做到和调用本地方法一样,让开发人员更专注于业务开发,不用去考虑网络编程等细节。

RPC 框架怎么就实现不让开发人员关注网络编程等细节呢?

首先我们区分两个角色一个服务提供方,一个是服务调用方。服务调用方其实是通过动态代理、负载均衡、网络调用等机制去服务提供方的机器上去执行对应的方法。服务提供方将方法执行完成后,将执行结果再通过网络传输返回到服务提供方。 大致过程如下:

但是现在的服务都是集群部署,那么服务调用方怎么能够实时的知道服务提供方的集群中的变化,例如服务提供方的 IP 地址变了,或者是服务重启时怎么能够及时的切换流量呢?

这就需要{注册中心} 起作用了,我们可以把注册中心看作服务端,然后每个服务都看成客户端,每个客户端都需要将自己注册到注册中心,然后一个服务调用方要调用另一个服务时,需要从注册中心获取服务提供方的信息,主要是获取服务提供方的服务器 IP 地址列表和端口信息。

服务调用方获取到这些信息后缓存到自己本地,并且跟注册中心保持一个长连接当服务提供方有任何变化时,注册中心能够实时的通知给服务调用方,调用方能够及时更新自己本地缓存的信息(也可以采用定时轮询的方式)。

服务调用方获取到服务器 IP 地址信息后,根据自己的负载均衡策略选择一个 IP 地址然后发起网络调用的请求。

那么网络客户端是通过什么发起的网络调用呢?

可以自己使用 JDK 原生的 BIO 或者 NIO 来实现一套网络通信模块,但是这里我们建议直接使用强大的网络通信框架 Netty。它是基于 NIO 的网络通信框架,支持高并发,封装完善,而且性能好传输快。

Netty 不是我们本文的主要内容,这里就不展开说了。

客户端调用过程

因为我们知道数据在网络中传输的时候都是以二进制的形式的,所以在调用方将调用的参数进行传递的时候是需要进行序列化的。服务提供方在接收到参数时也是需要进行反序列化的。

网络协议

调用方既然需要序列化,服务提供方又要进行反序列化,这样双方就要确定好一个协议,调用方传输什么参数,服务提供方就按照这个协议去进行解析,而且在返回结果的时候也是按照这个协议进行结果解析。

那么这个协议应该是怎么样的结构,都是什么样子的呢? 因为这个协议可以自定义,我们为了方便就以 JSON 的形式给举个例子:

{"interfaces": "interface=com.jimoer.rpc.test.producer.TestService;method=printTest;parameter=com.jiomer.rpc.test.producer.TestArgs","requestId": "3","parameter": {"com.jiomer.rpc.test.producer.TestArgs": {"age": 20,"name": "Jimoer"}}
}

首先第一个参数interfaces是,我们要让服务提供方知道调用方要调用哪个接口,以及接口中的哪个方法,并且方法的参数是什么类型的。

第二个参数是当前一次请求的一个唯一标识,在多个线程同时请求一个方法时,用这个 id 来进行区分,以后无论是做链路追踪还是日志管理都可以以此 id 为依据。

第三个参数就是 实际的调用方法中的参数值。具体是什么类型的,每个属性值都是什么。

调用

下面也是举一个简单的例子来说明一下调用的过程。我们一部分采用代码的形式一部分采用文字的形式来将整个调用过程串起来。

// 定义请求的URL
String tcpURL = "tcp://testProducer/TestServiceImpl";
// 定义接口请求
TestService testService = ProxyFactory.create(TestService.class, tcpURL);
// 组装请求参数
TestArgs testArgs = new TestArgs(20,"Jimoer");
// 通过动态代理执行请求
String result = testService.printTest(testArgs);

通过查看上面的代码我们可以看到整个调用过程最核心的地方在 ProxyFactory.create() 方法里,这个方法里面主要的过程是,动态代理生成接口的实际代理对象,然后使用 Netty 的接口发起网络请求。

Proxy.newProxyInstance(getClass().getClassLoader(), interfaces.getClass().getInterfaces(), new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 第一步:获取调用服务的地址列表ListregistryInfos = interfacesMethodRegistryList.get(clazz);if (registryInfos == null) {throw new RuntimeException("无法找到服务提供者");}// 第二步: 通过自身的负载均衡策略选择一个地址RegistryInfo registryInfo = loadBalancer.choose(registryInfos);// 第三步:Netty的网络请求处理ChannelHandlerContext ctx = channels.get(registryInfo);// 第四步:根据接口类的全路径名和方法生成唯一标识String identify = InvokeUtils.buildInterfaceMethodIdentify(clazz, method);String requestId;// 第五步:通过加锁的方式保证生成的requestId的唯一性synchronized (ApplicationContext.this) {requestIdWorker.increment();requestId = String.valueOf(requestIdWorker.longValue());}// 第六步: 组织参数JSONObject jsonObject = new JSONObject();jsonObject.put("interfaces", identify);jsonObject.put("parameter", param);jsonObject.put("requestId", requestId);System.out.println("发送给服务端JSON为:" + jsonObject.toJSONString());// $$ 多条消息之间的分隔符String msg = jsonObject.toJSONString() + "$$";ByteBuf byteBuf = Unpooled.buffer(msg.getBytes().length);byteBuf.writeBytes(msg.getBytes());// 第七步:这里发起调用ctx.writeAndFlush(byteBuf);// 这里会将线程进行阻塞,知道服务提供方将请求处理好之后返回结果,再唤醒。waitForResult();return result;}});

执行过程大致分为这几步:

  1. 获取调用服务的地址列表。
  2. 通过自身的负载均衡策略选择一个地址。
  3. Netty 的网络请求处理(选择一个渠道 Channel)。
  4. 根据接口类的全路径名和方法生成唯一标识。
  5. 通过加锁的方式保证生成的 requestId 的唯一性。
  6. 组织请求参数。
  7. 发起调用。
  8. 线程阻塞,直到服务提供方返回结果。
  9. 填充返回结果,返回到调用方。
服务端处理过程

上面也说了,服务调用方发起网络请求后,会阻塞住,直到服务提供方返回数据,所以服务提供方处理完调用方法的逻辑后,还是要唤醒阻塞的调用线程的。

服务提供方在处理请求时也是先通过 Netty 获取到数据,然后再进行反序列化,然后再根据协议获取到需要调用的方法,然后通过反射去进行调用。

Netty 的返回入口在下面这部分逻辑里

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {try {String message = (String) msg;if (messageCallback != null) {// 将接收到的消息放到回调方法中messageCallback.onMessage(message);}} finally {ReferenceCountUtil.release(msg);}
}

Netty 的 client 接收到响应的消息后,先将结果返回到调用方,处理完成之后再去释放之前的阻塞调用线程。

client.setMessageCallback(message -> {// 这里收单服务端返回的消息,先压入队列RpcResponse response = JSONObject.parseObject(message, RpcResponse.class);System.out.println("收到一个响应:" + response);String interfaceMethodIdentify = response.getInterfaceMethodIdentify();String requestId = response.getRequestId();// 设定唯一标识String key = interfaceMethodIdentify + "#" + requestId;Invoker invoker = inProgressInvoker.remove(key);// 将结果设置到代理对象中invoker.setResult(response.getResult());// 加锁再释放之前的阻塞线程。synchronized (ApplicationContext.this) {ApplicationContext.this.notifyAll();}
});

setResult() 方法

@Override
public void setResult(String result) {synchronized (this) {this.result = JSONObject.parseObject(result, returnType);notifyAll();}
}

上面的步骤就是这样,按照之前请求的唯一标识放入到返回的信息中,然后将结果设置到代理对象中,再通过返回结果,然后唤醒之前的调用阻塞线程。

总结

其实整个 RPC 的请求过程就是如下(不含异步调用):

做一个总结,用大白话把一个 RPC 请求流程描述出来: 首先无论是调用方还是服务提供方都要注册到注册中心;

  1. 服务调用方把请求参数对象序列化成二进制数据,通过动态代理生成代理对象,通过代理对象,使用 Netty 选择一个从注册中心拉取到的服务提供方的地址,然后发起网络请求。
  2. 服务提供方从 TCP 通道中接收到二进制数据,根据定义的 RPC 网络协议,从二进制数据中反序列化后,分割出接口地址和参数对象,再通过反射找到接口执行调用。
  3. 然后服务提供方再把调用执行结果序列化后,回传到 TCP 通道中。
  4. 服务调用方获取到应答二进制数据后,再反序列化成结果对象。

这样就完成了一次 RPC 网络调用,其实后面框架扩展后,还要考虑限流、熔断、服务降级、序列化多样性扩展,服务监控、链路追踪等等功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/431015.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

neo4j小白入门

1.建立几个学校的节点 1.1创建一个节点的Cypher命令 create (Variable:Lable {Key1:Value,Key2,Value2}) return Variable 1.2创建一个学校的节点 create (n:School{name:清华大学,code: 10003,establishmentDate:date ("1911-04-29")})return n 1.3一次创建几个…

出现conda不是内部或外部命令,也不是可运行的程序或批处理文件。的解决办法

发现是我的环境变量不对,需要改成conda.exe所在的目录下 如果不知道自己conda.exe在哪的 可以下载个everything这个软件 找东西很快 找到后 点击环境变量-系统变量-Path-新建-(你的conda.exe所在目录:绝对路径) 完成上述操作…

如何通过蜂巢(容器安全)管理内部部署数据安全产品与云数据安全产品?

本文将探讨内部部署和云数据安全产品之间的主要区别。在思考这个问题之前,首先了解内部部署和云数据安全产品之间的主要区别。 内部部署数据安全产品意味着管理控制台位于企业客户的内部部署,而德迅云安全则在云中托管云数据安全产品。德迅云安全供应商通…

DERT目标检测—End-to-End Object Detection with Transformers

DERT:使用Transformer的端到端目标检测 论文题目:End-to-End Object Detection with Transformers 官方代码:https://github.com/facebookresearch/detr 论文题目中包括的一个创新点End to End(端到端的方法)简单的理解就是没有使…

MISC - 第四天(OOK编码,audacity音频工具,摩斯电码,D盾,盲文识别,vmdk文件压缩)

前言 各位师傅大家好,我是qmx_07,今天继续讲解MISC知识点 FLAG 附件是一张图片,尝试binwalk无果 使用StegSolve工具Data Extract查看时 发现PK字段,是大多数压缩包的文件头点击Save Bin保存zip文件 解压缩失败使用修复软件:htt…

代码随想录Day17 图论-2

103. 水流问题 本题思路很简单 要求我们找到可以满足到达两个边界的单元格的坐标 有一个优化的思路就是 我们从边界的节点向中间遍历 然后用两个数组表示 一个是第一组边界的数组 一个是第二边界的数组 如果两个数组都遍历到了某一个单元格 就说明该单元格时满足题目要求的 #…

OpenLayers 开源的Web GIS引擎 - 添加地图控件地图控件

中心点按钮、地图放大缩小滑块、全图和比例尺控件 直接上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.…

如何选择适合的干式电抗器?

干式电抗器是电力系统中重要的电气设备&#xff0c;主要用于限制电网中的短路电流&#xff0c;提高电力系统的稳定性和可靠性。选择适合的干式电抗器对于保障电力系统的正常运行具有重要意义。以下是选择适合的干式电抗器的一些建议&#xff1a; 1. 根据电力系统的需求选择合适…

大模型算法岗常见面试题100道(值得收藏)非常详细收藏我这一篇就够了

大模型应该是目前当之无愧的最有影响力的AI技术&#xff0c;它正在革新各个行业&#xff0c;包括自然语言处理、机器翻译、内容创作和客户服务等等&#xff0c;正在成为未来商业环境的重要组成部分。 截至目前大模型已经超过200个&#xff0c;在大模型纵横的时代&#xff0c;不…

【设计模式】创建型模式(四):建造者模式

《设计模式之创建型模式》系列&#xff0c;共包含以下文章&#xff1a; 创建型模式&#xff08;一&#xff09;&#xff1a;工厂模式创建型模式&#xff08;二&#xff09;&#xff1a;抽象工厂模式创建型模式&#xff08;三&#xff09;&#xff1a;单例模式创建型模式&#…

Android14请求动态申请存储权限

Android14请求动态申请存储权限 Android14和Android15存储权限有增加多了选择部分&#xff0c;还是全部。一个小小的存储权限真的被它玩出了花来。本来Android13就将存储权限进行了3个细分&#xff0c;是图片&#xff0c;音频还是视频文件。 步骤一&#xff1a;AndroidManife…

峟思:山洪灾害监测预警系统全面解析

在自然灾害频发的今天&#xff0c;山洪灾害以其突发性强、破坏力大而备受关注。为了有效预防和减少山洪灾害带来的损失&#xff0c;山洪灾害监测预警系统应运而生。本文将详细介绍该系统的主要组成部分、关键传感器及其工作机制&#xff0c;以期为防灾减灾工作提供有力支持。 山…

项目小总结

这段时间主要把大概的开发流程了解完毕 修改了&#xff0c;并画了几个界面 一.界面 修改为 博客主页 个人中心 二.前后端分离开发 写前端时 就可以假设拿到这些数据了 const blogData2 {blog:{id:1,title: "如何编程飞人",author_id: 1,content: "这是一篇…

最新版C/C++通过CLion2024进行Linux远程开发保姆级教学

目前来说&#xff0c;对Linux远程开发支持相对比较好的也就是Clion和VSCode了&#xff0c;这两个其实对于C和C语言开发都很友好&#xff0c;大可不必过于纠结使用那个&#xff0c;至于VS和QtCreator&#xff0c;前者太过重量级了&#xff0c;后者更是不用说&#xff0c;主要用于…

【论文阅读】Grounding Language with Visual Affordances over Unstructured Data

Abstract 最近的研究表明&#xff0c;大型语言模型&#xff08;llms&#xff09;可以应用于将自然语言应用于各种各样的机器人技能。然而&#xff0c;在实践中&#xff0c;学习多任务、语言条件机器人技能通常需要大规模的数据收集和频繁的人为干预来重置环境或帮助纠正当前的…

vue node node-sass sass-loader 版本 对应 与 兼容

警告&#xff1a; LibSass 和 Node Sass 已弃用。虽然它们将继续无限期地接收维护版本&#xff0c;但没有计划添加其他功能或与任何新的 CSS 或 Sass 功能兼容。仍在使用它的项目应该转移到 Dart Sass。 sass Sass是一种预处理器脚本语言&#xff0c;可以解释或编译成…

Java—反射机制详解

介绍反射 反射的基本概念 反射&#xff08;Reflection&#xff09;是Java语言中的一种机制&#xff0c;它允许程序在运行时检查和操作类、接口、字段和方法等类的内部结构。通过反射&#xff0c;你可以在运行时获取类的信息&#xff0c;包括类的构造器、字段、方法等&#xf…

在 Windows 上运行 Vue 项目时解决 ‘NODE_OPTIONS‘ 错误

在 Windows 上运行 Vue 项目时解决 ‘NODE_OPTIONS’ 错误 在 Windows 系统上启动 Vue 项目时&#xff0c;遭遇报错。具体报错信息如下&#xff1a; ‘NODE_OPTIONS‘ 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。这个错误通常意味着 Windows 系统无法识…

机器翻译之创建Seq2Seq的编码器、解码器

1.创建编码器、解码器的基类 1.1创建编码器的基类 from torch import nn#构建编码器的基类 class Encoder(nn.Module): #继承父类nn.Moduledef __init__(self, **kwargs): #**kwargs&#xff1a;不定常的关键字参数super().__init__(**kwargs)def forward(self, X, *args…

基于SpringBoot+Vue+MySQL的美食点餐管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 在数字化快速发展的今天&#xff0c;餐饮行业也迎来了转型升级的重要机遇。传统餐饮管理方式面临效率低下、顾客体验不佳等问题。为此&#xff0c;开发一款基于SpringBootVueMySQL架构的美食点餐管理系统显得尤为重要。该系统旨…