0. 起因
最近看文档,发现一些组件是通过FastRPC来进行沟通的,并且偶尔看到某些场景下在FastRPC上的时间消耗好像也蛮可观,恰好FastRPC是开源的,因此决定看看FastRPC具体的实现。
1. RPC简介
当初在学Java的时候,初遇RMI(Remote Method Invocation),感觉这是一个非常神奇的东西,竟然可以调用别的机器上的方法!多么好奇这是怎么实现的。后来渐渐明白,其实RMI就是个RPC的另一个名字。那么,RPC的原理是什么呢?
RPC全称Remote Procedure Call(远程过程调用),所谓远程,就是执行的例程(routine)位于另一个地址空间(关于地址空间的介绍可以参阅之前的文章——冰山之下:使用new申请内存的背后
简单说可以认为地址空间就是进程),通常情况下是另一台机器。如图1所示,RPC框架为调用者提供一个代理,这个代理中的方法和远程例程一一对应。当调用者调用了代理的一个方法后,代理通过一种客户端——服务器的方式来,将函数签名、参数等封装成一个网络请求,发送给服务器。服务器接收到请求后通过解析获得函数签名、参数等信息,通过查表获取到目标例程地址,调用该例程并将参数传递给它。例程计算完成后,将结果通过网络返回到代理,代理将服务器返回的结果稍加处理,成为调用者可以理解的数据格式,最终返回到调用者。客户端和服务器的沟通细节是通过RPC框架来实现的,数据的序列化和反序列化、网络请求与答复等细节是隐藏的,对于调用者来说,它并不知道它调用的方法来对数据的处理到底是自己的地址空间进行还是在别的地址空间进行。
举个栗子:你到一家餐厅吃饭,店里有一个服务员为你服务。你点完菜之后,服务员就走进了后厨。但是,服务员并不是直接将你点的菜告诉他们店里的厨师,相反的,他打电话到别的店点了外卖,外卖到了之后他将饭菜重新装盘,然后给你端了上来。但是你是不知道这个细节的,在你看来,这次吃饭和以往的任何一次没有区别,服务员端上的饭菜在哪里做的你是不知道的,你和平常一样点了菜然后菜就上了,你并不需要自己去关心怎么找到外卖电话,怎么将饭菜装盘。你只管点菜和吃,除了可能等的时间比以往略久,没有任何区别。这个栗子中,服务员就扮演着RPC 中代理的角色,你就是调用者,外卖店就是服务器。
情况就是这么个情况,但是由于RPC只是一种实现方法,并没有形成标准,因此有着很多不同的实现,比如gRPC、Dubbo、Thrift和FastRPC等。对于如何进行序列化和反序列化、如何通信,不同的框架有着不同的实现。
2. FastRPC简介
FastRPC是一个XML-RPC协议的实现,它的特点是有多种数据序列化方式可选:二进制、JSON、XML以及Base64,因为它使用HTTP协议作为载体,通过HTTP的头的数据格式协商字段很容易知道数据的格式。
FastRPC相对与其他框架来说非常简单,代码主要就三部分:客户端实现、服务端实现以及数据序列化与反序列化的实现。也是因为它太简单了,它并没有涉及到鉴权等方面的内容,这些需要自己去考虑。但是对于了解一个RPC框架的具体实现已经足够了。
3. FastRPC调用流程
RPC分为两部分:运行在本地址空间的客户端部分以及运行在其他地址空间的服务器部分。下面就来看看FastRPC对于着两部分的实现。
3.1. FastRPC Server调用流程
FastRPC Server的实现如图2所示,其工作流程如下:
- 将客户端可以调用的例程依次进行注册,存放到一个注册表,其实就是一个以函数名为键、函数地址为值的一个字典;
- 例程注册完成后,开始启动对特定的端口的监听;
- 当有请求到达后,通过HTTP的头确定内容的序列化格式,调用对应的反序列化方法对数据进行解析。
- 解析完成获得函数名和参数,通过查表或取到函数地址,调用函数并将参数传入;
- 函数返回后通过将数据返回请求者。
整个过程非常简单。
3.2. FastRPC Client的调用流程
FastRPC Client的实现如图3所示,其工作流程如下:
- 首先进行链接的配置,例如服务器地址、端口号、最大等待时间等;
- 配置完成后等待客户调用,客户调用特定方法,传入了参数。客户端调用选定的序列化例程对数据进行序列化,并且根据数据填充HTTP头信息;
- 客户端发起HTTP请求,将数据发送个服务器,等待服务器应答;
- 获取到服务器返回的数据后,对数据进行解析,返回给调用者。
整个请求的核心就是下面的几行代码,位于frpcserverproxy.cc
中,首先将方法名和参数都序列化到本地的缓存,然后通过flush
方法写到HTTP输出流:
// fastrpc\src\frpcserverproxy.cc
...
try {
marshaller->packMethodCall(methodName);
// marshall all passed values until null pointer
while (const Value_t *value = va_arg