在上一个章节,我们了解到为什么要使用远程调用,以及在远程调用过程中所经历的步骤。下面我们就以小蓝和小红的对话为例,详细来讲一下为什么一次调用的过程会经过这么多的步骤,以及它们的作用与意义。
首先我们想一下,小蓝和小红如何通过网络进行对话呢?回想学过的知识,我们可以使用 Socket 建立一个 TCP 连接,然后他们两人就可以通过 Socket 来收发通话信息了,就像下面这样:
1)传输层的作用
不过这里有一个问题,数据在网络中是以二进制流的方式持续传输的,那如何分清楚一条消息数据的开始和结束呢?比如像下面这样,“小蓝”连续对“小红”说了三科成绩(这就相当于连续进行了三次 RPC 调用),这种情况下“小红”是搞不清楚他在说什么的:
所以这里我们要引入一个被称为“传输层”的东西。这样在数据发送出去前,我们先将数据交给“传输层”,由“传输层”对每一次要发送的数据进行封装,然后再通过 Socket 发送出去。如此以来我们就可以解决上面的问题。
那么“传输层”要将数据封装成什么样子呢?比如,我们可以将数据封装成下图这种样子,在数据前加上一个 4 个字节长度的内容,用来表示数据的长度。这样“小红”接收到数据后,读取前 4 个字节就能知道本次数据的长度,然后再继续读取对应长度的数据就得到了本次传输内容。而超过这个长度后如果还有数据,则为下一次请求中的数据。
2)协议层的作用
上面我们通过“传输层”封装了每次调用的数据,目的是确保接收方可以正确的取出对应数据(不会多或少于发送的数据)。
而现在我们又发现一个问题,就是传输的数据有可能会比较复杂,可能是一个复杂的数据对象或者复杂的集合类型,但是我们通过网络传输时,传输的内容是字节流。所以我们必须得想办法,把这个复杂的数据对象或集合转换成字节流,然后才能交给“传输层”装帧发送出去。
比如下面就是一个复杂的参数,它是一个我们自定义的类,有两个字段。这个时候我们就要想办法将这个对象中的数据,序列化成字节流的形式,再交给传输层去传输。
public class Solve {
int seq = 1;
String opt = "A";
}
所以这时我们可以加入“协议层”去做这个事情,通过“协议层”将复杂的数据转换成字节流,再经过“传输层”封装后,就可以交给 Socket 发送出去了。
那么“协议层”是怎么做的呢?本质上“协议层”是从对象的各个属性中取出来数据,按“某种规则”拼接成了字节数组。而这里的“某种规则”可以是“JSON”,那么也就是说我们可以将对象数据转换成 JSON 样式的字符串,再从字符串转化为字节数组。下面就是将对象中的转换成 JSON 样式字符串的效果。
{"seq":1,"opt":"A"}
这样通过引入“协议层”我们就解决了复杂数据的传输问题。有了上述的两层后,我们的 RPC 就基本可用了。每次想通过 RPC 调用对方时,先使用“协议层”将参数序列化,然后再使用“传输层”在数据的最前面加上长度信息并发送出去。
3)处理器的作用
上面我们说有了“协议层”和“传输层”后就基本可用了,为什么是基本可用呢?是因为上述过程只适用于被调方是单个方法的情况,这样接收到参数后,去执行这个方法就行。但实际我们肯定不止一个方法,如果我们只发送参数信息过去,那谁能知道你需要调用哪个方法呢?
所以我们要把方法名称也一起发送给被调用方,这样调用方就能区分出来调用的是哪个方法。调用方根据方法名称去匹配到对应的方法,并执行调用。而去做这件事情的代码我们将其称为“处理器”,也就是说由“处理器”来选择要目标方法并进行调用。
如下图中我们就已经添加上了“处理器”,这样由“处理器”拿到本次请求的方法名称后,寻找到对应的要执行的方法,调用并将参数传入,等方法执行完成再将结果返回交给协议层封装处理。
4)客户端代理类
另外,加上上述这么多的层次后,整个 RPC 调用过程就变得复杂了起来,如果我们每次写 RPC 功能时都要去这么多逻辑,那就太繁琐了。而前面我们也说过,RPC 框架的作用就是让我们在调用远程方法时,能够像调用本地方法一样简便,那 RPC 框架是怎么做到这一点的呢?
为了能让我们使用 RPC 时能和本地方法一样,不需要关注内部这些数据转化和传输的繁琐过程,我们再引入一个“代理”的概念。即 RPC 框架在客户端根据接口生成一个“接口代理类”,然后客户端直接调用该接口代理类即可。在代理中我们获取方法中传来的参数,并将参数和方法信息通过协议层序列化,再通过传输层发送出去。
5)编解码器
我们可以为每个接口类都定制化的生成一个接口代理类,专门为这个接口服务,但是这样做代码冗余太多,接口一旦有改动就要关联的去修改代理类,并不是很方便。所以接口的代理类一般都是一段通用的代码,能够代理任何接口。
但既然代理类是通用的代码,那么就要考虑如何将参数读取出来,再通过协议层序列化呢?因为通用的代码我们无法直接通过参数 get 方法获取值,这时候我们想起来 gson 或 fastjson 等 JSON 格式转换工具,通过它们就可以将参数数据直接转换成 JSON 格式的数据,而它们本质上是通过反射处理的这一切。同样 Thrift 也提供了通过反射机制读取参数数据的工具,就是编解码器。Thrift 解析接口及参数,生成对应的编解码器,通过编解码器与协议层的配合使用,Thrift 就能将参数数据序列化了。
增加接口代理和编解码器后,使得 RPC 框架整体的使用舒适度上大大提升,使用 RPC 调用时只需和普通调用一样调用接口方法即可,后面的工作全都由代理类去处理。
6)服务管理与模型
客户端通过代理类屏蔽了 RPC 调用过程中的细节,而服务我们也加上一层服务类,也屏蔽掉服务端处理 RPC 调用过程的细节,让我们在开发 RPC 的服务端时不需要考虑太多,只需要实现接口对应的业务逻辑即可。这样服务类需要做的就是去请求数据、反序列化数据,再通过编解码器将数据组装成参数由处理器匹配到目标方法执行调用。
这样以来,我们就得了完整的 RPC 结构模型,
以上就是 RPC 框架的层次结构,通过上述描述大家应该能了解到各个层次的存在的意义以及用途。下一章我们就来看一下如何使用 Thrift 这个 RPC 框架进行开发。