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

news2024/12/25 8:59:12

注册中心

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() {
            @Override
            public 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.coloradmin.cn/o/2139919.html

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

相关文章

【开源免费】基于SpringBoot+Vue.JS企业客户管理系统(JAVA毕业设计)

本文项目编号 T 036 ,文末自助获取源码 \color{red}{T036,文末自助获取源码} T036,文末自助获取源码 目录 一、系统介绍1.1 管理员角色1.2 普通员工角色1.3 系统特点 二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内…

苹果手机备份照片怎么删除

在数字时代,备份照片是保护我们珍贵记忆不受意外丢失影响的一种重要方式。苹果手机用户通常利用iCloud或iTunes来备份他们的照片,确保数据的安全。然而,随着时间的推移,这些备份可能会积累大量不再需要的照片,占用宝贵…

鸿蒙开发之ArkTS 基础二

ArkTS常用的基础数据类型 1.字符串 关键字是string 2.数字 关键字是number 3.布尔 关键字是boolean 语法格式是:let 变量名:变量类型 变量值 其中let是关键表示变量,可以修改,可以改变一只对应的是const 修饰,常量不能修改,…

Python画笔案例-050 绘制天空之眼

1、绘制天空之眼 通过 python 的turtle 库绘制 天空之眼,如下图: 2、实现代码 绘制 天空之眼,以下为实现代码: """天空之眼.py """ import math import turtledef draw_square(length,level):if l…

idea同时装了两个版本,每次打开低版本都需要重新激活破解

问题描述: idea同时装了两个版本,每次打开低版本都需要重新激活破解。低版本是2021.1,高版本是2023.1 解决方案: 找到idea的配置路径,比如我的是:C:\Users\Administrator\AppData\Roaming\JetBrains 2021…

【我要成为配环境高手】Nodejs安装与配置

文章目录 1.nodejs安装2.配置npm的全局安装路径3.切换npm的淘宝镜像4.安装vue-cli 1.nodejs安装 从官网下载安装LTS版本的nodejs nodejs会自动安装环境变量,因此安装完成后直接在cmd中查看node版本 node -v2.配置npm的全局安装路径 以管理员身份运行cmd&#xff…

office 2021安装教程

软件介绍 Microsoft Office是微软公司开发的一套基于 Windows 操作系统的办公软件套装。常用组件有 Word、Excel、Powerpoint等。该软件最初出现于九十年代早期,最初是一个推广名称,指一些以前曾单独发售的软件的合集。当时主要的推广重点是购买合集比单…

matlab边缘点提取函数

1、边缘提取 matlab自带点云边缘提取函数,用于搜索点云边界,其核心是alpha shapes算法。alpha shapes提取边缘点,主要是依据滚动圆绕点云进行旋转,实现边缘检测,原理如下图所示。具体原理及效果,可以参考之前我写的博客:基于alpha shapes的边缘点提取(matlab)-CSDN博客…

实习项目|苍穹外卖|day10

Spring Task cron 表达式 入门案例 订单状态定时处理 通知用户支付!通知商家完成订单! Scheduled(cron "0 0/1 * * * ? ")public void processTimeoutOrder(){log.info("定时处理超时订单: {}", LocalDateTime.now());//答案是…

黑马程序员Java笔记整理(day01)

1.windowsR进入运行,输入cmd 2.环境变量 3.编写java第一步 4.使用idea 5.注释 6.字面量 7.变量 8.二进制 9.数据类型 10.关键词与标识符

仿真软件PROTEUS DESIGN SUITE遇到的一些问题

仿真软件PROTEUS DESIGN SUITE遇到的一些问题 软件网上有很多下载地址自己找哈! 首先如果遇到仿真 没有库 ,需要在网上下载库文件替换到DATA目录下 如果不是默认安装到C盘需要手动修改这些地址,不然会报错!! 当遇到点击仿真出现报错 : 检查这个设置地址是否正确: 随便在库文…

Unity3D 小案例 像素贪吃蛇 02 蛇的觅食

Unity3D 小案例 像素贪吃蛇 第二期 蛇的觅食 像素贪吃蛇 食物生成 在场景中创建一个 2D 正方形,调整颜色,添加 Tag 并修改为 Food。 然后拖拽到 Assets 文件夹中变成预制体。 创建食物管理器 FoodManager.cs,添加单例,可以设置…

周期冲激函数

指数函数的求和----真周期冲击 指数函数有限积分----假单个冲击 指数函数无限积分----真单个冲击

职业院校数据科学与大数据技术专业人工智能实训室建设方案

一、引言 随着人工智能(AI)技术的迅猛发展,其在全球范围内的应用日益广泛,从智能交通、环境保护到公共安全、智能家居等多个领域均展现出巨大的潜力。然而,我国在人工智能领域的人才储备仍显不足,这已成为…

8. 尝试微调LLM大型语言模型,让它会写唐诗

这篇文章与3. 进阶指南:自定义 Prompt 提升大模型解题能力一样,本质上是专注于“用”而非“写”,你可以像之前一样,对整体的流程有了一个了解,尝试调整超参数部分来查看对微调的影响。 这里同样是生成式人工智能导论&a…

华为HarmonyOS地图服务 -- 三种地图类型 -- HarmonyOS9

一. 场景介绍 Map Kit支持以下地图类型: STANDARD:标准地图,展示道路、建筑物以及河流等重要的自然特征。NONE:空地图,没有加载任何数据的地图。TERRAIN:地形图。 1 标准地图: …

7.1溪降技术:徒步

目录 7.1 徒步运动概述观看视频课程电子书:徒步路线选择故事时间不稳定地形 7.1 徒步 运动概述 徒步是溪降活动中不可或缺的一部分,我们在下降峡谷时大部分时间都在徒步。随着我们进入更具挑战性的峡谷,能够高效移动将使我们更加自信和安全。…

Semaphore UI --Ansible webui

1、安装python python下载地址 https://www.python.org/downloads/ 选好版本下载 wget https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tar.xz安装编译工具 sudo dnf groupinstall "Development Tools"安装依赖包 dnf install bzip2-devel ncurses-deve…

react18基础教程系列-- 框架基础理论知识mvc/jsx/createRoot

react的设计模式 React 是 mvc 体系,vue 是 mvvm 体系 mvc: model(数据)-view(视图)-controller(控制器) 我们需要按照专业的语法去构建 app 页面,react 使用的是 jsx 语法构建数据层,需要动态处理的的数据都要数据层支持控制层: 当我们需要…

时序预测 | Matlab实现SSA-TCN麻雀搜索算法优化时间卷积网络时序预测-递归预测未来数据(单输入单输出)

时序预测 | Matlab实现SSA-TCN麻雀搜索算法优化时间卷积网络时序预测-递归预测未来数据(单输入单输出) 目录 时序预测 | Matlab实现SSA-TCN麻雀搜索算法优化时间卷积网络时序预测-递归预测未来数据(单输入单输出)预测效果基本介绍…