本文简要介绍RPC的相关知识。
1 概述
RPC(Remote Procedure Call),即远程过程调用,是一种通过网络从远程计算机程序上请求服务、而不需要了解底层网络技术的协议。RPC协议假定某些传输协议(如TCP或UDP)的存在,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发应用程序(包括网络分布式多程序在内)更加容易。
例如,有两台服务器A和B,一个应用部署在服务器A上,想要调用服务器B上的应用提供的函数/方法。由于服务器A和服务器B的应用程序不在一个内存空间,不能直接调用,就需要通过网络来传达调用的语义和调用的数据,这就是RPC协议。
2 背景
在单机时代,一台电脑上运行多个进程,为了实现进程之间的通信,就出现了IPC(Inter-process communication,单机中运行的进程之间的相互通信)。
而到了网络时代,大家的电脑都互相连起来了,以前程序只能调用自己电脑上的进程,能不能调用其他机器上的进程呢?为了实现这个目的,把IPC扩展到网络上,就是RPC(远程过程调用)了。
3 原理
本节通过本地过程调用和远程过程调用来介绍RPC的原理。
3.1 本地过程调用
在研究RPC前,我们先看看本地调用是怎么调的。
假设我们要调用函数Multiply来计算“lvalue * rvalue”的结果,代码如下:
1 int Multiply(int l, int r) {
2 int y = l * r;
3 return y;
4 }
5
6 int lvalue = 10;
7 int rvalue = 20;
8 int l_times_r = Multiply(lvalue, rvalue);
在程序执行到第8行时,实际上执行了以下操作:
- 将lvalue和rvalue的值压栈;
- 进入Multiply函数,取出栈中的值10和20,将其赋予l和r;
- 执行第2行代码,计算“l * r”,并将结果存在y;
- 将y的值压栈,然后从Multiply返回;
- 回到第8行,从栈中取出返回值200,并赋值给l_times_r。
上述5步就是执行本地调用的过程。
3.2 远程过程调用(RPC)
实际上RPC就是要像调用本地的函数一样去调远程函数。
在进行远程过程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,3.1节中提到的Multiply函数是在另一个机器上的进程中执行的。这就带来了下面几个问题。
1. 我们怎么告诉远程机器我们要调用Multiply,而不是Add或者其他函数呢?
解决方法:Call ID映射。
在本地调用中,函数体是直接通过函数指针来指定的。我们调用Multiply,编译器就自动帮我们调用它对应的函数指针。但是在远程调用中,调用函数指针的方法是行不通的,因为两个机器的两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID,这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。同时,我们还需要在客户端和服务端分别维护一个“函数与Call ID”的对应表,客户端和服务端上的对应表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程过程调用时,它就查一下这个表,找出想要调用的函数相应的Call ID,然后把它(Call ID)传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
2. 客户端怎么把参数值传给远程的函数呢?
解决方法:序列化和反序列化。
在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的(机器的)进程,不能通过内存来传递参数,甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python),这时候就需要客户端把参数先转成一个字节流,传给服务端后,服务端再把这个字节流转成自己能读取的格式,这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
3. 网络传输问题
远程过程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把“Call ID”和“序列化后的参数字节流”传给服务端,然后再把“序列化后的调用结果”传回客户端,只要能完成这两个功能的,都可以作为传输层使用。因此,RPC所使用的协议其实是不限的,只要能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。
综上所述,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。Call ID映射可以直接使用函数字符串,也可以使用整数ID,映射表一般就是一个哈希表;序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的;网络传输库可以自己写Socket,或者用Asio,ZeroMQ,Netty之类。
4 简要流程
一次远程过程调用的简要流程如下(对照流程图理解下列的具体流程):
1. 首先,要解决通信的问题。主要是通过在客户端和服务器之间建立连接(如TCP连接),远程过程调用的所有数据都在这个连接中传输。此连接可以是按需连接,调用结束后就断掉,即短连接;也可以是长连接,多个远程过程共享同一个连接。
2. 其次,要解决寻址问题。也就是说,服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口、方法名称(IP+端口+方法名),这样才能完成调用。
3. 然后,client上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议(如TCP)传递到server上。由于网络协议是基于二进制的,内存中的参数也要序列化成二进制的形式,这就是序列化过程(Serizlize),通过连接寻址,将序列化的二进制发送给server。
4. 再然后,server收到请求后,需要对参数进行反序列化,恢复为内存中的表达形式,然后找到对应的方法(根据CALL ID与函数的对应表),进行本地调用,然后得到函数的返回值。
5. 最后,server需要将函数返回值发送给client上的应用。该返回值也需要经过序列化后发送,client接到server发送的消息后,再对该消息进行反序列化,恢复为内存中的表达形式,交给client上的应用。