最早开始听说 RPC(Remote Procedure Call,远程过程调用)是在 Windows 上,偶尔运行某个程序的时候,会弹出一个错误提示:远程过程调用(RPC)失败。 当时一直在写界面相关的程序,并没有多接触系统内部相关的开发,一直不太懂什么是远程过程调用。直到去年,我转行到了服务端开发,写了不少的微服务应用,远程过程调用 是微服务之间绝佳的通信方式,才逐渐认识到远程过程调用的重要性。
而回到 Windows 系统上,远程过程调用一般是在跨进程、跨模块(DLL,动态链接库)之间通信时的通信方式的一种叫法。 早期的时候,在有些语言中,没有返回值的函数有一个单独的名字:过程(Procedure)。而现在的几乎所有语言都不再有这样的区分,统称为函数(Function)。 所以,远程过程调用就是远程函数调用。只是名字一直这样叫并被保留了下来。
在写后端程序的过程中,也遇到很多次跟前端同事讲解过什么是 RPC,所以,今天这篇文章用简单的文字、简单的例子介绍什么是 远程过程调用(RPC)。
普通的函数调用
这里所说的普通的函数调用是相对于远程过程调用而言的。调用函数和被调用函数通常在同一个地方实现。
考虑下面这一段简化的代码:
1 2 3 4 5 6 7 8 |
|
上面的代码中,main
函数调用了sum
函数进行两个数的求和计算。非常简短的一段代码,main
和sum
两个函数在同一个文件里面实现,不是远程过程(函数)调用。
远程过程调用
远程过程调用中,被调用的函数的实现放在了“远程”。这个远程,可以是:另一个线程中,另一个进程中,另一台主机中(同一个网络或非同一个网络均可)。
下面的代码举两台主机之间 RPC 调用的例子,一台主机叫客户端,一台主机叫服务端。
服务端:函数实现
首先服务端(函数实现方)监听端口运行一个服务,该服务提供一个sum
函数的真实实现:
1 2 3 |
|
客户端:函数存根(Stub)与调用转发
客户端是指函数调用方。它有一个与函数几乎实现完全一样的原型(函数签名):
1 2 3 4 5 6 7 8 |
|
客户端的sum
函数和原型(函数签名)和服务端几乎完全一样:输入参数和输出参数(返回值)。这样的客户端实现我们一般把它叫作存根(Stub)。
不同的地方在于:客户端的函数不是具体实现,它通过网络等方式调用服务端的实现。而这一过程,需要将输入参数传递给服务端,需要接收服务端的返回值。
用伪代码和注释描述上述“其它代码”的具体逻辑,客户端的sum
函数的实现是下面这样:
1 2 3 4 5 6 7 8 |
|
可以看到,客户端的sum
函数本身并没有实现sum
函数的真正逻辑,而是把此调用处理过程转发给了服务端的实现。
参数与返回值的处理
远程过程调用通常采用 TCP 网络传输数据,而 TCP 是基于流数据的字节传输协议。所以参数和返回值有“打包”和“解包”过程。 它们有正式的名字,分别是:序列化(Serialization)、反序列化(Unserialization/Deserialization),或者另一对名字:列集(Marshal)、散集(Unmarshal)。
序列化之前的对象中的成员等结构在内存中存放的位置几乎总是不连续的,零散地分布在各个地方,这样的零散数据非常不便于传输。通过序列化可以将它们平坦地放在一段连续的内存中,这样可以非常方便地采用 TCP(字节流传输)协议进行传输。反序列化与之类似,但过程相反。
举个例子,如果你懂 JavaScript 的话。JavaScript 中有两个函数:JSON.stringify(对象)
和JSON.parse(字符串)
。前者能把一个对象(或其它任意JSON值)转换成一个字符串,后者能把一个字符串还原成一个对象(或其它任意JSON值)。这就是序列化与反序列化的过程。
如果你不懂 JavaScript 的话,也没关系。那你一定读写过文件。简而言之:把一段数据写入到文件即序列化,从文件恢复出该数据即反序列化。文件写入是一个按字节顺序写入的过程,本质跟数据在 TCP 网络上传输并无区别。
远程过程调用图示
注:客户端到服务端的连接一般是长连接,不会在每次调用时单独建立一个连接。所以图中没有画出连接建立部分。