一次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.mzph.cn/pingmian/54404.shtml

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

相关文章

演示jvm锁存在的问题

文章目录 1、AlbumInfoApiController --》testLock()2、redis添加键值对3、AlbumInfoServiceImpl --》testLock() 没有加锁4、使用ab工具测试4.1、安装 ab 工具4.2、查看 redis 中的值 5、添加本地锁 synchronized6、集群情况下问题演示 jvm锁:synchronized lock 只…

尚品汇-H5移动端整合系统(五十五)

目录: (1)运行前端页面 (2)启动前端页面 (3)添加搜索分类接口 (4)购物车模块修改 (5)登录模块 (6)订单模块 &#…

Golang | Leetcode Golang题解之第423题从英文中重建数字

题目: 题解: func originalDigits(s string) string {c : map[rune]int{}for _, ch : range s {c[ch]}cnt : [10]int{}cnt[0] c[z]cnt[2] c[w]cnt[4] c[u]cnt[6] c[x]cnt[8] c[g]cnt[3] c[h] - cnt[8]cnt[5] c[f] - cnt[4]cnt[7] c[s] - cnt[6]…

【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL16

使用8线-3线优先编码器Ⅰ实现16线-4线优先编码器 描述 ②请使用2片该优先编码器Ⅰ及必要的逻辑电路实现16线-4线优先编码器。优先编码器Ⅰ的真值表和代码已给出。 可将优先编码器Ⅰ的代码添加到本题答案中,并例化。 优先编码器Ⅰ的代码如下: module…

[python]从零开始的PySide安装配置教程

一、PySide是什么? PySide 是 Qt for Python 项目的一部分,它提供了与 PyQt 类似的功能,使开发者能够使用 Python 编程语言来构建基于 Qt 的图形用户界面 (GUI) 应用程序。PySide 是由 Qt 公司官方维护的,而 PyQt 则是由第三方开发…

【Pyside】pycharm2024配置conda虚拟环境

知识拓展 Pycharm 是一个由 JetBrains 开发的集成开发环境(IDE),它主要用于 Python 编程语言的开发。Pycharm 提供了代码编辑、调试、版本控制、测试等多种功能,以提高 Python 开发者的效率。 Pycharm 与 Python 的关系 Pycharm 是…

【JavaEE】——多线程(join阻塞,计算,引用,状态)

阿华代码,不是逆风,就是我疯,你们的点赞收藏是我前进最大的动力!!希望本文内容能够帮助到你! 目录 一:join等待线程结束 1:知识回顾 2:join的功能就是“阻塞等待” …

java之斗地主部分功能的实现

今天我们要实现斗地主中发牌和洗牌这两个功能,该如何去实现呢? 1.创建牌类:52张牌每一张牌包含两个属性:牌的大小和牌的花色。 故我们优先创建一个牌的类(Card):包含大小和花色。 public class Card { //单张牌的大小及类型/…

无人机+自组网:中继通信增强技术详解

无人机与自组网技术的结合,特别是通过中继通信增强技术,为无人机在复杂环境中的通信提供了稳定、高效、可靠的解决方案。以下是对该技术的详细解析: 一、无人机自组网技术概述 无人机自组网技术是一种利用无人机作为节点,通过无…

proteus仿真学习(1)

一,创建工程 一般选择默认模式,不配置pcb文件 可以选用芯片型号也可以不选 不选则从零开始布局,没有初始最小系统。选用则有初始最小系统以及基础的main函数 本次学习使用从零开始,不配置固件 二,上手软件 1.在元件…

6--SpringBootWeb案例(详解)

目录 环境搭建 部门管理 查询部门 接口文档 代码 删除部门 接口文档 代码 新增部门 接口文档 代码 已有前端,根据接口文档完成后端功能的开发 成品如下: 环境搭建 1. 准备数据库表 (dept 、 emp) -- 部门管理 create table dept( id int un…

如何有效检测住宅IP真伪?

在当今的互联网时代,住宅IP(即家庭用户通过宽带服务提供商获得的IP地址)在跨境电商、广告投放、网络安全等多个领域扮演着重要角色。然而,随着网络环境的复杂化和欺诈行为的增多,如何有效检测和辨别住宅IP的真伪成为了…

Spring:统一结果私有属性造成的前端无法访问异常报错问题

用户未填写任何评价 1.问题复现 (1)看一段代码 controller: import lombok.extern.slf4j.Slf4j; import org.ljy.testdemo.common.Result; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.w…

Centos中关闭swap分区,关闭内存交换

概述: Swap 分区是 Linux 系统中扩展物理内存的一种机制。Swap的主要功能是当全部的RAM被占用并需要更多内存时,用磁盘空间代理RAM内存。Swap对虚拟化技术资源损耗非常大,一般虚拟化是不允许开启交换空间的,如果不关闭Swap&…

【Linux课程学习】make/Makefile:Linux项目自动化构建工具

🎁个人主页:我们的五年 🔍系列专栏:Linux课程学习 🌷追光的人,终会万丈光芒 🎉欢迎大家点赞👍评论📝收藏⭐文章 🍉一.make/Makefile的理解: …

关于STM32项目面试题02:ADC与DAC篇(输入部分NTC、AV:0-5V、AI:4-20mA和DAC的两个引脚)

博客的风格是:答案一定不能在问题的后面,要自己想、自己背;回答都是最精简、最精简、最精简,可能就几个字,你要自己自信的展开。 面试官01:什么是模数转换/ADC?说说模数转换的流程? …

基于SpringBoot+Vue+MySQL的手机销售管理系统

系统展示 用户前台界面 管理员后台界面 商家后台界面 系统背景 随着智能手机的普及和市场竞争的日益激烈,手机销售行业面临着前所未有的挑战与机遇。传统的手工记录和简单的电子表格管理方式已难以满足现代手机销售业务的需求,销售数据的混乱和管理效率低…

技术成神之路:设计模式(十四)享元模式

介绍 享元模式(Flyweight Pattern)是一种结构性设计模式,旨在通过共享对象来有效地支持大量细粒度的对象。 1.定义 享元模式通过将对象状态分为内部状态(可以共享)和外部状态(不可共享)&#xf…

C语言-文件操作-一些我想到的、见到的奇怪的问题

博客主页:【夜泉_ly】 本文专栏:【C语言】 欢迎点赞👍收藏⭐关注❤️ C语言-文件操作-一些我想到的、见到的奇怪的问题 前言1.在不关闭文件的情况下,连续多次调用 fopen() 打开同一个文件,会发生什么?1.1过…

Cursor火出圈,未来程序员还有出路吗?

大家好,我是凡人。 今天我表弟家邻居的阿姨,托他问问我目前程序员还有前景吗,希望我根据十几年的经验给出点建议,看看程序员这条路未来能不能走。 一下子不知道该怎么回复他了,如果是三年前问我,肯定毫不…