前言

面试的时候经常被问到RPC相关的问题,例如:你说说RPC实现原理、让你实现一个RPC框架应该考虑哪些地方、RPC框架基础上发起一个请求是怎样一个流程等等。所以这次我就总结一波RPC的相关知识点,提前说明一下,本篇文章只是为了回答一些面试问题,所以只是解释原理,并不会深入挖掘细节。

注册中心

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

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

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

大致过程如下:

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

这就需要$\color{red}{注册中心}$起作用了,我们可以把注册中心看作服务端,然后每个服务都看成客户端,每个客户端都需要将自己注册到注册中心,然后一个服务调用方要调用另一个服务时,需要从注册中心获取服务提供方的信息,主要是获取服务提供方的服务器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网络调用,其实后面框架扩展后,还要考虑限流、熔断、服务降级、序列化多样性扩展,服务监控、链路追踪等等功能。这些就要后面再扩展的讲了,这次就先到这了。

参考:

如何设计一个短小精悍、可拓展的RPC框架?(含实现代码)

一篇文章了解RPC框架原理

你说说RPC的一个请求的流程是怎么样的?的更多相关文章

  1. Spring MVC 处理一个请求的流程分析

    Spring MVC是Spring系列框架中使用频率最高的部分.不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分.因此程序员一定要熟练掌握MV ...

  2. 《Symfony 5全面开发》教程04、Symfony处理http请求的流程

    当我们访问项目的/test路径时,浏览器会向我们的服务器端发送一个请求.我们打开浏览器的开发者工具,打开network选项卡. 在地址栏中敲击回车,浏览器向我们的服务器端发送了一个请求.请求的地址是1 ...

  3. 1.说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?说说一次 rpc 请求的流程?

    作者:中华石杉 面试题 说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?说说一次 rpc 请求的流程? 面试官心理分析 MQ.ES.Redis.Dubbo,上来先问你一些思考性的问题.原 ...

  4. Net 一个请求的处理流程

    Net 一个请求的处理流程   1.浏览器请求 请求-准备环境-->处理请求   2.Aspnet 环境的创建 客户请求 IIS区分静态文件还是动态文件,静态文件直接文件返回,动态文件通过asp ...

  5. 一个请求在Struts2框架中的处理流程

    1.客户端向Servlet容器发起一个请求,将请求封装为HttpServletRequest对象. 2.HttpServletRequest首先经过web.xml中配置的struts2的过滤器,以及s ...

  6. springmvc处理一个请求的全流程

    首先,用户的浏览器发出了一个请求,这个请求经过互联网到达了我们的服务器. Servlet 容器首先接待了这个请求,并将该请求委托给 DispatcherServlet 进行处理. 接着 Dispatc ...

  7. Redola.Rpc 的一个小目标

    Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标:20000 tps. Concurrency level: 8 threads Complete requests: 20000 ...

  8. JMeter 将上一个请求的结果作为下一个请求的参数——使用正则提取器(转载)

    在接口测试和压力测试过程中,经常会将几个流程串联起来才能测试.如:我要进行获取用户信息接口测试,我就要先登录成功后,才能获取用户信息.所以,我就要首先要登录,获得我的登录凭证(tokenId或tick ...

  9. jmeter 如何将上一个请求的结果作为下一个请求的参数——使用正则提取器

    1.简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具.用于对软件做压力测试,它最初被设计用于Web应用测试但后来扩展到其他测试领域. 它可以用于测试静态和动态资源例如 ...

随机推荐

  1. Django边学边记——中间件

    特点 Django中的中间件是一个轻量级.底层的插件系统,可以介入Django的请求和响应处理过程,用于全局修改Django的输入或输出. 每个中间件组件负责做一些特定的功能.中间件全部注册在sett ...

  2. 超详细unittest单元测试框架总结

    unittest单元测试框架不仅可以适用于单元测试,还可以适用WEB自动化测试用例的开发与执行,该测试框架可组织执行测试用例,并且提供了丰富的断言方法,判断测试用例是否通过,最终生成测试结果.今天笔者 ...

  3. 微信小程序自动化测试

    使用官方工具 使用webview测试方法,当2019年被微信封禁 使用native定位

  4. python 函数高级功能

    闭包 我们可以将闭包理解为一种特殊的函数,这种函数由两个函数的嵌套组成,且称之为外函数和内函数,外函数返回值是内函数的引用,此时就构成了闭包. # 闭包 # 外部函数的参数被内部函数引用,内部函数对外 ...

  5. django如何加载外部文件

    django如何加载外部文件(环境:pycharm python2.7 django1.11) 有一份新的文件夹名为:py_aiplat_demo,内含有多个文件夹(SDK,demo,data). 1 ...

  6. CVE-2012-0158 漏洞分析报告

      Office 2003 sp3(CVE-2012-0158)漏洞分析报告   软件名称:Office 2003 sp3 软件版本:2.0 漏洞模块:MSCOMCTL.ocx 模块版本:2.0.0. ...

  7. 鸿蒙内核源码分析(编译过程篇) | 简单案例窥视GCC编译全过程 | 百篇博客分析OpenHarmony源码| v57.01

    百篇博客系列篇.本篇为: v57.xx 鸿蒙内核源码分析(编译过程篇) | 简单案例窥视编译全过程 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙 ...

  8. P7470-[NOI Online 2021 提高组]岛屿探险【Trie,CDQ分治】

    正题 题目链接:https://www.luogu.com.cn/problem/P7470 题目大意 给出\(n\)个二元组\((a,b)\). \(q\)次询问给出\((l,r,c,d)\)表示询 ...

  9. 记一次centos挂载ceph存储的坑

    起因 生产有两台服务器,准备用来跑工作流,执行的资源的是放在ceph存储集群中,第一步挂载ceph 执行命令:mount -t ceph xxx:xxx -o name=admin,secret=AQ ...

  10. Python isinstance() 函数 Python 内置函数 Python 内置函数

    描述 isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type(). isinstance() 与 type() 区别: type() 不会认为子类是一种父类类型,不考虑继承关 ...