RPC原理详解与实现
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/109/ )。
概述
初看起来,远程过程调用(Remote Procedure Call,简称RPC)是一个比较复杂的名字。但是这个名字起得真的确实好,功能如其名:
- 远程:指的是需要经过网络的,而不是应用内部、机器内部进行的。
- 过程:也就是方法。
那“远程过程调用”,就是:可以跨过一段网络,调用另外一个网络节点上的方法。
以上就是对远程过程调用的简单理解。
背景
远程过程调用的广泛使用,是伴随着分布式系统的普及发生的。要了解它的背景,我们需要从这里开始讲起。
单体应用内部存在模块间的调用,这种调用发生在应用内。通常,一个模块可以直接调用另一个模块中的类、对象、方法,十分方便。并且这种调用发生在同一个机器内,甚至是同一个进程、线程内,十分高效。
应用之间也会存在调用,常基于接口实现。调用过程中会涉及到序列化、反序列化、网络传输、参数校验等多个环节,但因为其发生频率很低,也是可以接受的。
分布式应用内部的节点之间也会存在调用。这种调用由单体应用的模块间调用演化而来,其调用频率是相对较高的。但是,他们之间的调用已经无法通过应用的内部调用来实现,基于接口的调用则成本太高、效率太低。这时需要一种能够跨节点的、相对低成本和高效的方式来解决节点间的调用问题,这个问题如下图所示。
“高内聚、低耦合”是软件设计尤其是面向对象设计中的一个重要原则。依据此原则设计的软件系统,由下到上的每一层级都会提升自身的内聚性,降低与外界的耦合。最终使得整个系统中,层级越低的组织间耦合越高,层级越高的组织间耦合越低。
“高内聚、低耦合”这一原则最终会反映在组织间的互相调用上。最终,层级越低的组织间互相调用的频率越高,层级越高的组织间互相调用的频率越低。如上图所示。以上图所示的应用、节点、模块组成的三层的软件结构为例。应用处在最高层,应用间的耦合是很低的,它们间的调用是低频的。节点处在中间层,它们间的耦合居中,其调用频率也是居中的。模块处在最底层,它们之间的耦合最高,模块之间的调用频率也最高。
节点间的调用,也常被称为服务调用,是需要跨机器的,而其发生频率介于应用间调用和模块间调用之间,是相对高频的。因此需要一种相对简单高效的、支持跨机器的调用方式,这就是服务调用要解决的问题。
那显然这种调用方式要满足如下几点:
- 能够跨机器。因为分布式系统的节点往往部署在不同的机器上。
- 运行效率高。要能够简化各种不必要的包装环节,提升有效信息的比例。
- 使用方便。用户能够无感知地使用,降低进行调用时候的使用成本。这里主要包括透明性1,尤其是其中的访问透明性。
而远程过程调用则是满足上述要求的不错选择。
原理
远程过程调用使得服务可以像调用本地方法一样调用网络上另一个服务中的方法。可这一切是怎么实现的呢?接下来,我们介绍下它的具体原理。
在使用远程过程调用前,服务提供方需要将自身服务的接口文件导出。而服务调用方则要引入这些接口文件。
进行远程过程调用时,服务提供方只需要调用接口文件中的接口,便相当于调用了服务提供方中的具体实现方法,并得到服务提供方给出的执行结果。调用其他服务中的方法就像调用本地方法一样方便。如图所示。
接下来我们从服务调用方、通信、服务提供方三部分介绍远程过程调用的具体实现过程。
服务调用方
服务调用方在本地调用服务提供方给出的接口后,相当于远程调用了该接口的具体实现。这主要用到了动态代理。
动态代理能够为原本的空接口注入一个代理实现。于是,服务调用方对接口的调用便转化为了对代理实现的调用。
在代理实现中,会向服务注册中心查询服务提供方的具体地址、端口,并将调用参数序列化,然后向服务提供方发送调用请求。之后,代理实现还会接收请求的回应,并通过接口返回给服务调用方的业务逻辑。
通信
根据OSI(Open System Interconnect,开放式系统互联)模型,将网络通信的工作划分为图所示的七层。
RPC在工作时,通常使用第七层的HTTP协议或者第四层的TCP协议、UDP协议在两个节点之间进行通信。
相比而言,HTTP协议工作在第七层,对传输的信息进行了多层的封装,因此有用信息占比低,效率比较低,但使用更为简单;TCP协议、UDP协议工作在第四层,减少了封装的次数,因此有用信息占比高,效率比较高,但使用略为复杂。
服务提供方
服务提供方在接受到调用请求后,需要定位到请求所要调用的具体方法。然后将请求中的参数反序列化后展开对方法的实际调用。并在调用结束后将执行结果返回。
实践
那要想实现这个过程该怎么办呢?别急,咱们一步一步来。
首先,调用方调用的是接口,必须得为接口构造一个假的实现。显然,要使用动态代理。这样,调用方的调用就被动态代理接收到了。
第二,动态代理接收到调用后,应该想办法调用远程的实际实现。这包括下面几步:
- 识别具体要调用的远程方法的IP、端口
- 将调用方法的入参进行序列化
- 通过通信将请求发送到远程的方法中
第三,远程的服务接收到了调用方的请求后,它应该:
- 反序列化各个调用参数
- 定位到实际要调用的方法,然后输入参数,执行方法
- 按照调用的路径返回调用的结果
整个过程如下所示,虚线框内的部分是RPC框架实现的功能。
当然在具体实现中,还需要解决一些细节问题。例如,如何具体定位要调用的方法,如何确定参数的类型等等。而至于成熟的框架,则更为复杂了。
为了让大家更好地理解RPC的实现,我们直接编写了一个极简(可能是最简单的了)的RPC示例项目。该实例只用少量的几个类便实现了RPC功能,并且配有服务调用方和服务提供方的展示示例。成熟的框架要比我们的项目复杂的多,但越是简单的项目,越能让大家理解其基本原理。项目地址为:https://github.com/yeecode/EasyRPC
下面是客户端的代码结构,看着类有点多,其实代码不长。其中的RPC代码完成完成动态代理、远程调用参数序列化、远程调用发起、远程调用结果反序列化的工作。
下面是服务端的代码结构,代码更少,完成远程调用接收、调用参数反序列化、调用实际触发、调用结果序列化的工作。
这样,一个RPC小框架就做完了,并不复杂。
大家可以查看代码,了解其具体实现。
总结
RPC是易用的。它是一个涉及动态代理、序列化、通信、反序列化的复杂过程,但是,以上这些过程都被RPC框架隐藏了起来。在使用RPC框架时,我们只需要调用服务提供方的接口便可以调用到服务提供方的具体实现,而不用关心其实现细节。
RPC是高效的。它通过将底层的通信协议封装了起来,RPC可以基于第四层的协议来提升信息传输的效率,并可以自由设计通信格式。
但是,RPC也有副作用。RPC的使用使得服务提供方不需要给出HTTP接口。这对非RPC的调用是不友好的,可见,RPC调用损失了服务的通用性、可读性。
该回答参考了书籍《分布式系统原理与工程实践》。书中还介绍了RPC如何结合注册中心模型使用,如何在服务调用方的代理实现中完成服务提供方的地址查询等内容。想了解的同学可以自行阅读。
另外书中对于分布式系统中的一致性、共识、Paxos、分布式事务、服务治理、微服务、幂等、消息系统、Zookeeper都有很详细的介绍。对大家体系化地掌握这些知识很有帮助。
是一本理论联系实际的好书,推荐给大家。因为反响不错,这本书还发行了繁体版。
好了,我是程序员易哥。
别忘了点赞后再收藏。
这回就说这么多!
可以关注我!等我下回接着说!
- Tanenbaum A S, Van Steen M. 分布式系统原理与范型[J]. 2004. [return]
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。