远程过程调用(Remote Procedure Call, RPC)是从一台机器上通过参数传递的方式调用另一台机器上的一个函数或方法并得到返回的结果。这样的方法又可以称为服务。RPC隐藏了底层的通讯细节,不需要直接处理Socket通讯或Http通讯,并将这些细节封装成一个服务(Service)。RPC是一个请求响应模型。客户端发起请求,服务器返回响应。
在进行详细的阐述之前,RPC的请求响应模型可以简单的理解为类似于Http的工作方式。
在使用形式上,RPC像调用本地函数或方法一样,去调用远程的函数或方法。
为什么需要RPC
在设计一个非分布式应用时,程序各个模块之间的交互都通过方法调用进行数据传递。例如,dao模块通过定义接口的方式暴露一些方法,供给上层调用,业务逻辑层调用这些方法,传入一些参数,就可以对持久层的数据进行读写访问。这种单台主机就能完成的数据交互就叫做集中式运算(centralized computing)。当数据交互的需求转移到分布式运算(distributed computing)系统中时,由于本模块所需要的数据可能不在本地的内存或者持久层中,所以要通过网络调用的方式进行数据传输,网络传输的原始解决方案是使用socket。
Socket是Client/Server模型网络的基本组成部分。它们为程序提供了一个相对简单的机制,可以在远程或本地机器上建立与另一个程序的连接并来回发送消息。甚至可以使用Socket进行系统调用。但是原始的Socket必须使用输入/输出接口(input/output)来对分布式系统进行数据交互,这和传统的通过接口暴露方法服务的方式有很大差距,也不利于服务提供模块对底层服务的管理。为了解决这个局限性,1984年,Birrell和Nelson设计了一个机制,允许程序在其他机器上调用程序。主机A上的进程可以对主机B的过程进行调用,此时A上的进程被暂停并且继续执行B,当B返回时传递返回值给A并继续执行A中的进程。这种机制被称为远程过程调用(RPC)。RPC的主要目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。
本地方法调用的实现
由于编译器和系统结构的不同,本地方法的调用的过程可能会有不同。JVM使用基于栈的指令系统,这里主要分析基于栈的指令系统来分析。
处理器会提供某种类型的call
指令,这条指令会将堆栈中下一条指令的地址压入操作数栈,并将处理器的访问控制转移到由调用指定的地址。当调用结束后,通过一个return
指令,目标地址从栈顶弹出,处理器释放对该地址的控制权。在这个过程中,编译器会执行类似识别参数、压参数入栈、执行调用指令等细节。在被调用的函数中,编译器存储可能被标记为clobbered的寄存器的值,为本地变量分配栈帧,然后在返回之前恢复寄存器和释放栈空间。
指令系统共有四种分类:堆栈型,累加器型,寄存器-存储器型和寄存器-寄存器型。分类的依据是操作数的来源。
RPC通用架构及典型流程
本地方法调用没有一个步骤是涉及到网络的,为了令编译器模拟在不同系统之间的方法调用过程,需要一个比Socket更高级别的定义。它需要一个操作系统级别(operating system level)的协议构造,使远程过程不必通过Socket调用,而是通过一个语言级别(language-level)的架构。
远程过程调用协议的架构看起来是这个样子:
RPC图片来源:UNIX网络编程(UNIX Network Programming)英文版第693页,作者W. Richard Steven。
进行RPC的核心便是Stub中的函数。在客户端上,Stub中的函数看起来真的是本地调用的函数,例如dao定义的暴露给业务逻辑层调用的接口,但实际上包含通过网络发送和接收消息的代码。
什么是Stub?Stub是一段部署在分布式系统Client端的代码,一方面接收应用层的参数,并对其包装(pakage)后通过socket发送到Server端,另一方面接收Server端序列化后的结果数据,解除包装后交给Client端应用层。
Skeleton功能与Client Stub相反,部署在Server端,从传输层接收序列化参数,将包装的参数再转换后交给Server端的应用层,并将应用层的执行结果包装后最终传送给Client端Stub。
所以客户端调用一个client stub
的方法并获取返回结果,就完成了远程过程调用。在这之间的步骤大概如下。
- 客户端调用
client stub
中的过程(procedure),因为client stub
部署在客户端,所以调用起来和调用本地方法看上去是一样的。 -
client stub
对客户端传入的参数进行包装(package),例如进行特定RPC框架的标准格式转换,并包装后的数据构建成一个或者多个网络消息。这个过程成为编组(marshaling),需要将数据序列化为平面化的字节数组。 - 通过使用Socket接口对本地内核进行系统调用,
client stub
将网络消息发送到远程系统。 - 网络消息由内核通过某种协议(例如无连接的UDP,面向连接的TCP)传输到远程系统。
-
skeleton
对来自消息的参数进行解包装,将它们从标准网络格式转换成特定于机器的形式。 -
skeleton
使用这些参数调用server端的函数或者方法(对客户端来说即远程过程)。 -
skeleton
获取返回结果,并重新包装、编组为消息发送回客户端。 -
client stub
从本地内核读取网络中的结果并进行转换,返回给客户端。客户端代码继续执行,调用结束。
RPC相对于Socket的优势
RPC相对于原始的Socket接口,将所有的网络代码隐藏到Stub的函数或方法中,应用程序不必担心Socket、端口号、数据转换和解析等底层细节。带来的优势主要体现在以下两点:
- 使用过程调用语义来调用远程函数并获得响应
- 降低编写分布式应用程序的开发成本
RPC协议在协议分层体系中的位置
计算机网络中主流的协议分层体系即OSI和TCP/IP。在OSI参考模型上,RPC跨越会话层和展示层(session and presentation layers, 层5和层6)。TCP/IP协议族中,RPC属于应用层的内容。
RPC的优势
- 不用考虑端口号的选择问题,RPC封装了对可用端口号的选择并绑定。
- 独立于运输层,由于skeleton是代码自动生成的,所以会自动兼容传输层的协议。客户端也可以动态选择协议,因为发送和接收消息的代码是自动生成的,业务层无需关心这些问题。
- 客户端上的应用程序只需要知道一个传输地址:负责告诉应用程序在哪里连接一组给定的服务器功能的名称服务器。
- 使用函数调用模型代替Socket接口
RPC实现要点
实现一个RPC,一般有如下几点需要考虑。
参数传递
一般来说参数传递可以有引用传递和值传递。由于远端和本地端内存位置可能有差异,所以引用传递是没有意义的。如果是引用型数据结构,必须转换成平面结构。例如树必须转换成扁平化树(flattened tree)。
数据表示
在本地系统上不存在数据不兼容问题,因为数据格式总是一样的。而网络调用中,远程机器可能具有不同的字节顺序,不同的整数大小以及不同的浮点表示。所以在网络上传输的数据需要进行序列化。
IP协议强制对header中的所有16位和32位字段使用大端字节排序。而RPC中需要一个标准的数据结构。一般有隐式(implicit typing)和显式(explicit typing)两种
- 隐式:只传输值,而不是变量的名称或类型。
- 显式:每个字段的类型与值一起传输。
JSON、Protocol Buffers、XML都是显式格式。
传输协议
某些RPC实现只允许使用一个传输层协议(例如TCP)。大多数RPC支持实现多个并允许用户选择。
主机和端口绑定
我们需要找到一台远程主机,并在主机上找到合适的进程(端口或传输地址)。一个解决方案是维护一个中央数据库,可以找到一个提供服务类型的主机。
此外还需要考虑错误重试、性能、安全性问题。
Stub代码生成
很多主流的开发语言并没有生成Stub代码的语法,所以不能直接支持RPC。一个比较通用的解决办法是提供一个编译器把Client/Server Stub代码编译出来:
Stub由于RPC的跨平台特性,所以需要一个平台无关的语言来描述这种RPC接口的特性,这样的语言就叫做接口描述语言(interface definition language, IDL)。加入IDL后,整个流程大概就是这个样子:
图片来自美团点评技术团队:序列化和反序列化接口描述语言
接口描述语言用于描述接口定义,接口定义就像是Java接口的方法声明,RPC编译后,这些声明就会织入端中。WebService的WSDL(XML为基础)是一个IDL的例子。
很多数据表示格式本身也作为RPC中序列化的数据结构,也能作为IDL编译Stub代码。比如Yaml、Json、xml、PB等等,这些都可以作为接口描述语言。
关于IDL的选择参考Api 体系架构分享(上)
早期的RPC
第一代RPC不支持对象的传递,比较典型的有ONC RPC,OSF RPC。
面向对象的RPC
随着面向对象语言的流行,现有的RPC机制和框架实现虽然能够完成需求,但是不支持面向对象的操作。例如从远程类中实例化一个对象、跟踪对象实例、多态支持等。加入这些面向对象的支持成为一种趋势。
微软的COM和DCOM
COM,包括后来的DCOM、COM+,都并没有真正实现跨平台,它们主要用于Windows,是微软实现的RPC框架。COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。
想一下这个场景,工程师需要是简单的序列化协议,但却要先掌握语言编译器,可想而知。
和大多数早期RPC系统一样,DCOM也不能很好地跨越防火墙,因此防火墙必须允许流量在ORPC和DCOM使用的某些端口之间流动。
CORBA
CORBA是早期比较好的实现了跨平台,跨语言的序列化协议(RPC框架)。CORBAR为了解决异构平台的 RPC,首先使用IDL来定义远程接口,并将其映射到特定的平台语言中。但是CORBA太复杂,其参与方过多带来的版本过多,版本之间兼容性又差,最终导致COBRA的消亡。
J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。
CORBA的IDL看起来像这个样子:
Module StudentObject {
Struct StudentInfo {
String name;
int id;
float gpa;
};
exception Unknown {};
interface Student {
StudentInfo getinfo(in string name)
raises(unknown);
void putinfo(in StudentInfo data);
};
};
Java RMI
Java RMI没有IDL,所以也就不支持跨平台,但是对于Java程序员而言显得更直接简单,降低使用的学习成本。
除了跨平台,Java RMI还有一个问题是其序列化机制。使用Java序列化写出元数据(meta-data)是非常昂贵的。Java Serializable序列化不仅写入完整类名,也包含整个类的定义,包含所有被引用的类。类定义可以是相当大的,可能会构成性能和效率的问题,当然这是编写一个单一的对象。如果序列化大量相同的类的对象,这时类定义的开销通常不是一个大问题。除此之外,如果对象有一个类的引用,那么Java序列化将写入整个类的定义,不只是类的名称,
支持Web的RPC
传统的RPC协议也可以在Web下工作,但是它的Socket端口是动态选择的。但是防火墙可能为了安全性限制大部分端口的访问,只允许开启某些协议端口,并检查协议格式是否正确,例如是否是一个正确的HTTP请求。
XML-RPC
XML-RPC是1998年设计的一种RPC消息传递协议,用于将程序请求和响应编入人可读的XML中。XML-RPC中,参数使用XML格式作为数据结构,并通过HTTP协议传输,不必为RPC服务器应用程序打开其他端口,解决了传统的企业防火墙的端口限制问题。
XML每个字段的类型与值一起传输,是显式传输的数据结构。
SOAP和Web Service
SOAP(Simple Object Access Protocol)是随着XML-RPC的流行而发展起来的一种规范化的对象传输协议。
由于该协议一点儿也不简单,并且不限于访问对象,因此该首字母缩略词已被丢弃。
SOAP使用XML格式作为无状态消息交换格式,支持包括RPC式的过程调用以及multipart响应。但是SOAP只是提供一个标准的消息传递结构,为了正确创建SOAP消息,还要一种描述服务的方法。Web Service使用WSDL(Web Services Description Language)来描述Web Service的服务。这是一个XML文档,可以被送入一个程序,该程序将生成将发送和接收SOAP消息的软件。WSDL就是一个IDL,用于生成Stub。
SOAP和XML-RPC的区别:
- SOAP设计更复杂,功能更强大
- XML-RP不需要对参数命名,按传入顺序传入和读出;SOAP通过参数名确定参数,无顺序。
- XML-RPC更适配Python
SOAP和XML-RPC的区别可以查看Difference Between RPC and SOAP
REST
REST(Representational State Transfer)遵循Web原则,使用HTTP作为协议的核心部分。并将HTTP中的PUT
/GET
/POST
/DELETE
操作和对资源的insdert
/select
/updaate
/delete
操作对应。
REST的思想是使用HTTP的命令来对数据进行获取和操作。REST使用URL来引用资源和相应操作。URL作为HTTP的一部分,提供了分层命名(hierachical naming)格式和参数属性值列表(attribute-value lists)。
REST不是RPC,但有一个类似的请求-响应模型。生成请求、消息生成和解析响应不是REST的一部分。但是REST对面向资源的服务很有意义,例如使用HTTP发送如下URL请求:
HTTP GET //www.xxxx.com/parts
返回一个包含请求数据的某种格式的数据结构,如JSON:
{"data":"null"}
JSON
使用JSON作为数据格式来发送数据已经十分流行。由于它不是二进制数据格式,所以更适合作为HTTP消息的载体。
但是JSON受JavaScript语言子集的限制,可表示的数据类型不够多,而且无法表示数据内的复杂引用,如自引用,互引用和循环引用。另外,某些语言具有多种JSON版本的实现,但在类型影射上没有统一标准,存在兼容性问题。
JSON只是一种消息传递格式,JSON不会尝试提供RPC库并支持服务发现、绑定、托管和垃圾回收。使用JSON作为数据转换格式的RPC成为JSON-RPC。JSON-RPC 虽然有规范,但是却没有统一的实现。在不同语言中的各自实现存在兼容性问题,无法真正互通。
无需IDL?
JSON来源于JavaScript中的"Associative array",由于"Associative array"在弱类型语言中本身就是类的概念,所以在这些弱语言如JavaScript、PHP中得到了良好的支持。并且,因为JSON中的字段一般可以和类中的属性名称和值一一对应,所以对于Java这强类型语言可以通过反射操作统一转换。
Google Protocol Buffers
Google Protocol Buffers本质上只是一种序列化机制,并不是完整的RPC。它仅仅简化了网络传输中编组(marshaling)和解组(unmarshaling)的流程。protobuf为结构化数据的序列化提供了一种高效的机制,使得将数据编码到网络上并解码接收到的数据变得容易。Protobuf定义了一种平台独立的数据结构类型,使得其序列化后的数据十分紧凑,解析非常高效。与很多IDL类似,protobuf的消息结构以高级格式定义,包含名称、类型和值。protobuf既可用于类似RPC的消息传递,也可用于持久性存储,您需要将数据转换为标准的串行形式以将其写入文件。一个例子是:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
和XML相比,protobuf生成的字节码短了2.5倍,解析速度快50倍左右。
其他常见的RPC框架优缺点可以参考什么是RESTful?到底REST和SOAP、RPC有何区别?中其中一个回答。
文献
RPC的拆解和实现可以查阅RPC的概念模型与实现解析
引用
Remote Procedure Calls - Paul Krzyzanowski
Difference Between RPC and SOAP
序列化和反序列化 - 美团点评技术团队
Api 体系架构分享(上)
网友评论