RPC 核心,万变不离其宗

微信搜 「yes的练级攻略」干货满满,不然来掐我,回复【123】一份20W字的算法刷题笔记等你来领。 个人文章汇总:https://github.com/yessimida/yes 欢迎 star !

Hola,我是 yes。

在了解 Dubbo 之前有必要先来剖析一波 RPC ,先搞清 RPC 原理再去深入了解 Dubbo 会起到事半功倍的效果。

理解核心原理很重要,市面上所有 RPC 框架都逃不过这些核心

搞清原理之后再看 Dubbo 就会有我说的那个熟悉感和“认可感”。

其实 RPC 不仅仅用在我们平日微服务的调用中,在很多和网络通信相关的场景都能用到 RPC。

比如消息队列客户端和 Broker 之间的交互,还有和一些其他中间件的交互都会用到  RPC。

你可能会说没啊?哪有 RPC 调用?

嘿嘿,你看这就是 RPC 的作用,让你无感知地完成了远程通信。

其实在中已经提过一次 RPC,也提到了 HTTP 和 RPC 的区别,不过那次的主角是 HTTP 。

这次咱们深入剖析一波 RPC,从根上来理解一波。

来,上车!

RPC 核心,万变不离其宗

正文

RPC 全称是 Remote Procedure Call ,即远程过程调用,其对应的是我们的本地调用。

远程其实指的就是需要网络通信,可以理解为调用远程机器上的方法。

那可能有人说:我用 HTTP 调用不就是远程调用了,那不也叫 RPC 了?

不是的,RPC 的目的是:让我们调用远程方法像调用本地方法一样无差别。

来看下代码就很清晰,比如本来没有拆分服务都是本地调用的时候方法是这样写的:

public String getSth(String str) {          return yesService.get(str);     }

如果 yesSerivce 被拆分出去,此时需要远程调用了,如果用 HTTP 方式,可能就是:

public String getSth(String str) {         RequestParam param = new RequestParam();         ......         return HttpClient.get(url, param,.....);     }

此时需要关心远程服务的地址,还需要组装请求等等,而如果采用 RPC 调用那就是:

  public String getSth(String str) {         // 看起来和之前调用没差?哈哈没唬你,         // 具体的实现已经搬到另一个服务上了,这里只有接口。         // 看完下面就知道了。          return yesService.get(str);       }

所以说  RPC 其实就是用来屏蔽远程调用网络相关的细节,使得远程调用和本地调用使用一致,让开发的效率更高。

在了解了 RPC 的作用之后,我们来看看 RPC 调用需要经历哪些步骤。

RPC 调用基本流程

按上面的例子来说,yesService 服务实现被移到了远程服务上,本地没有具体的实现只有一个接口。

那这时候我们需要调用 yesService.get(str) ,该怎么办呢?

我们所要做的就是把传入的参数和调用的接口全限定名通过网络通信告知到远程服务那里。

然后远程服务接收到参数和接口全限定名就能选中具体的实现并进行调用。

业务处理完之后再通过网络返回结果,这就搞定了!

RPC 核心,万变不离其宗

上面的操作这些就是由yesService.get(str) 触发的。

不过我们知道 yesService 就是一个接口,没有实现的,所以这些操作是怎么来的?

是通过动态代理来的。

RPC 会给接口生成一个代理类,所以我们调用这个接口实际调用的是动态生成的代理类,由代理类来触发远程调用,这样我们调用远程接口就无感知了。

动态代理想必大家都比较熟悉,最常见的就是 Spring 的 AOP 了,涉及的有 JDK 动态代理和 cglib。

在 Dubbo 中用的是 Javassist,至于为什么用这个其实梁飞大佬已经写了博客说明了。

他当时对比了 JDK 自带的、ASM、CGLIB(基于ASM包装)、Javassist。

经过测试最终选用了 Javassist。

梁飞:最终决定使用JAVAASSIST的字节码生成代理方式。虽然ASM稍快,但并没有快一个数量级,而JAVAASSIST的字节码生成方式比ASM方便,JAVAASSIST只需用字符串拼接出Java源码,便可生成相应字节码,而ASM需要手工写字节码。

可以看到选择一个框架的时候性能是一方面,易用性也很关键。

说回 RPC 。

现在我们知道动态代理屏蔽了 RPC 调用的细节,使得用户无感知的调用远程服务,那调用的细节有哪些呢?

序列化

像我们的请求参数都是对象,有时候是定义的  DTO ,有时候是 Map ,这些对象是无法直接在网络中传输的。

你可以理解为对象是“立体”的,而网络传输的数据是“扁平”的,最终需要转化成“扁平”的二进制数据在网络中传输。

RPC 核心,万变不离其宗

你想想,各对象分配在内存不同位置,各种引用,这看起来是不是有种立体的感觉?

最终都是要变成一段01组成的数字传输给对方,这种就01组成的数字看起来是不是很“扁平”?

把对象转化成二进制数据的过程称为序列化,把二进制数据转化成对象的过程称为反序列化。

当然如何选择序列化格式也很重要。

比如采用二进制的序列化格式数据更加紧凑,采用 JSON 等文本型序列化格式可读性更佳,排查问题比较方便。

还有很多序列化选择,一般需要综合考虑通用性、性能、可读性和兼容性。

具体本文就不分析了,之后再专门写一篇分析各种序列化协议的。

RPC 协议

刚才也提到了只有二进制数据才能在网络中传输,那一堆二进制在底层看来是连起来的,它可不会管你哪些数据是哪个请求的。

但接收方得知道呀,不然就不能顺利的把二进制数据还原成对应的一个个请求了。

于是就需要定义一个协议,来约定一些规范,制定一些边界使得二进制数据可以被还原。

比如下面一串数字按照不同位数来识别得到的结果是不同的。

RPC 核心,万变不离其宗

所以协议其实就定义了到底如何构造和解析这些二进制数据。

我们的参数肯定比上面的复杂,因为参数值长度是不定的,而且协议常常伴随着升级而扩展,毕竟有时候需要加一些新特性,那么协议就得变了。

一般 RPC 协议都是采用协议头+协议体的方式。

协议头放一些元数据,包括:魔法位、协议的版本、消息的类型、序列化方式、整体长度、头长度、扩展位等。

协议体就是放请求的数据了。

通过魔法位可以得知这是不是咱们约定的协议,比如魔法位固定叫 233 ,一看我们就知道这是 233 协议。

然后协议的版本是为了之后协议的升级。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssssf.html