RPC是一种方便的网络通信编程模型。由于与编程语言的高度集成,大大降低了处理网络数据的复杂度,大大提高了代码的可读性。但是RPC本身的组成比较复杂。由于编程语言、网络模型和使用习惯的限制,有很多妥协和取舍。本文通过分析几个流行的RPC实现案例,为大家设计RPC系统提供参考。由于RPC的底层网络开发一般都与具体的使用环境相关,编程实现方式也非常多样,但不影响用户,所以本文基本涉及如何实现一个RPC系统。理解RPC(remotecall)我们在各种操作系统和编程语言生态系统中都或多或少地接触过“远程调用”的概念。一般来说,它们是指使用简单的一行代码通过网络调用另一台计算机上的某个程序。例如:RMI——RemoteMethodInvoke:调用远程方法。一个“方法”一般是附加在一个对象上的,所以通常RMI是指在远程计算机上的一个对象上调用它的方法函数。RPC-RemoteProcedureCall:远程过程调用。指调用网络上另一台计算机上的特定功能代码。远程调用本身是一个网络通信的概念,其特点是将网络通信封装成类函数调用。除了远程调用,网络通信一般还有几个概念:包处理、消息队列、流过滤、资源拉取和等待。下面对比一下它们的区别:程序编程方式的信息封装传输模型的典型应用远程调用调用函数,输入参数,获取返回值。使用编程语言变量、类型和函数发送请求,并获得响应JavaRMI数据包处理调用Send()/Recv(),使用字节码数据,编解码器,处理内容将通信内容构造成二进制协议数据包发送/接收UDP编程报文queue调用Put()/Get(),使用“封装”对象,处理其中包含的内容,将消息封装成语言可用的对象或结构对于队列,存储一条消息;取出一个消息ActiveMQflowfilter读一个流,或者写一个流,立即处理流中的单位数据包,以统一的数据结构连接,单位长度较小;发送/接收;处理网络视频资源拉取并输入资源ID,获取资源内容请求或响应包含:header+text请求和等待响应WWWFeaturesforremotecalls-调用函数。业界已经开发了各种语言的类似解决方案,并且一些解决方案正在尝试实现跨语言。虽然远程调用在编程上看起来是最容易使用的,但它也有明显的缺点。因此,了解远程调用的优缺点是决定是否开发或使用远程调用模型的关键问题。远程调用的优点是:屏蔽了网络层。因此,在传输协议和编码协议方面,我们可以选择不同的方案。例如WebService方案采用HTTP传输协议+SOAP编码协议;而REST解决方案通常使用HTTP+JSON协议。Facebook的Thrift甚至可以自定义任何不同的传输协议和编码协议,你可以使用TCP+GoogleProtocolBuffer,或者UDP+JSON...。由于屏蔽了网络层,您可以根据实际需要自主优化网络部分,无需涉及业务逻辑处理代码,这对于需要在各种网络环境中运行的程序来说是非常有价值的。函数映射协议。您可以直接用编程语言编写数据结构和函数定义,而不用编写大量的编码协议格式和数据包处理逻辑。对于那些业务逻辑非常复杂的系统,比如网络游戏,可以节省大量定义消息格式的时间。而且函数调用模型非常容易学习,无需学习通信协议和流程,让经验不多的程序员也能轻松上手使用网络编程。远程调用的缺点:增加性能消耗。由于将网络通信包装成“函数”,需要进行大量的额外处理。例如,需要预生产代码,或者使用反射机制。这些操作会消耗额外的CPU和内存。而且,为了表达复杂的数据类型,比如变长类型string/map/list,必须在数据包中加入更多的描述信息,这样会占用更多的网络包长度。不必要的复杂。如果你只是为了一些特定的业务需求,比如传输一个固定的文件,那么你应该使用HTTP/FTP协议模型。如果是用于监控或者IM软件,使用简单的消息编码发送和接收会更快更高效。如果作为代理服务器,使用流式处理会非常简单。还有,如果你是做数据广播,消息队列很容易做,远程调用几乎不可能做。因此,最适合远程调用的场景是:业务需求在变化,网络环境在变化。RPC解决方案的核心问题既然远程调用使用的接口是一个“函数”,那么如何构建这个“函数”,就需要决定三个问题:1.如何表示“远程”信息所谓远程指网络上的另一个位置,则网络地址是必须输入的部分。在TCP/IP网络下,IP地址和端口号代表了运行程序的一个入口。因此需要指定IP地址和端口才能发起远程调用。但是,一个程序可能会运行很多功能,并接收到多个不同含义的远程调用。这样一来,如何让用户指定这些具有不同含义的远程调用条目就成了另一个问题。当然最简单的是每个端口一个调用,但是一个IP最多支持65535个端口,其他网络功能可能也需要端口,所以这个方案可能不够用,用一个数字来表示一个功能也不太好要想明白,必须查表才能明白。所以我们得想别的办法。在面向对象的思想下,提出了一些方案:用不同的对象概括不同的功能组合,先指定对象,再指定方法。这个思路很符合程序员的理解方式,EJB就是这种解决方案。一旦您决定使用对象模型来定义远程调用的地址,那么您就需要一种方法来指定远程对象。为了指定对象,您必须能够将有关对象的一些信息从被调用者(服务器)传输到调用者(客户端)。最简单的解决办法是,客户端输入一串串字符串作为对象的“名称”,发送给服务器,搜索用这个“名称”注册的对象。如果找到了,服务器就会使用某种技术把这个对象“传送”给客户端,然后客户端就可以调用他的方法了。当然,这种传输并不能将整个服务器上的对象数据复制到客户端,而是用一些符号或符号来表示服务器上的对象,然后再发送给客户端。如果你不是使用面向对象的模型,那么远程函数也必须被定位和传递,因为你调用的函数必须首先被找到,然后成为客户端的接口才能被调用。如何表达“远程对象”(这里所说的对象包括面向对象的对象或者只是函数)可以在网络上找到;一个重要的问题。2、函数的接口形式应该如何表示远程调用?由于网络通信的限制,它往往不能完全支持编程语言的所有特性。例如,C语言函数中的指针类型参数不能通过网络传递。因此,需要在设计方案中明确远程调用的功能定义,语言中哪些特性可以使用,哪些特性不能使用。如果这个规定过于严格,会影响用户的使用便利性;如果它太宽泛,可能会导致远程调用性能不佳。如何设计一种将编程语言中的函数描述为远程调用的函数的方式,也是需要考虑的问题。很多方案采用配置文件的通用方式,也有的可以直接在源码中添加特殊注释。一般来说,C/C++等编译型语言只能使用根据配置文件生成的源码,C#/JAVA等虚拟机语言可以结合配置文件使用反射机制(设置在源码中用特殊注释代替)code配置文件)方案,如果是脚本语言就更简单了,有时候连配置文件都不需要,因为脚本本身就可以行动。总之,远程调用接口必须满足什么样的约束,也是一个需要慎重考虑的问题。3、用什么方法实现网络通信?远程调用最重要的实现细节是关于网络通信的。使用什么通信方式来承载远程调用的问题又分为两个子问题:使用什么样的服务程序来提供网络功能?使用什么样的通信协议?远程调用系统可以直接编程TCP/IP来实现通信,也可以委托一些其他的软件,比如Web服务器,消息队列服务器等,也可以使用不同的网络通信框架,比如开源框架如内蒂/米娜。通信协议一般有两层:一层是传输协议,比如TCP/UDP或者更高级的HTTP,或者自己定义的传输协议;另一个是编码协议,就是在编程语言中如何序列化和反转对象。序列化成二进制字节流的方案,比较流行的方案有JSON、GoogleProtocolBuffer等,很多开发语言也有自己的序列化方案,比如JAVA/C#。上述技术细节具体使用哪一个,直接关系到远程调用系统的性能和环境兼容性。以上三个问题是远程调用系统必须考虑的核心选型。根据每个方案面临的不同约束,他们会在这三个问题上做出权衡,以适应他们的约束。但是现在没有“万能”或“万能”的解决办法。原因是:在如此复杂的系统中,如果需要照顾的特性越多,成本(易用性、性能开销)也越高。还会有更多。接下来我们可以研究一下业界的各种远程调用方案,看看他们在这三个方面是如何平衡和选择的。行业解决方案示例1.CORBACORBA是一个“古老”且雄心勃勃的解决方案。它试图在完成远程调用的同时完成跨语言通信的任务,所以它的复杂度是最高的,但是它的设计思想,也是后来被更多其他程序借鉴的。在通信对象的定位上,它使用URL来定义一个远程对象,这在互联网时代是很容易接受的。它的对象内容仅限于C语言类型,只能传值,也很好理解。为了让不同语言的程序能够相互通信,需要独立于各种编程语言设计一种只用于描述远程接口的语言。这就是所谓的IDL:InterfaceDescriptionLanguage接口描述语言。这样,你就可以先用一种超越所有语言的语言来定义接口,然后用工具自动生成各种编程语言的代码。这种方法几乎是编译语言的唯一选择。CORBA在通信问题上没有任何约定,而是交给特定语言的实现者去处理,这可能是它没有广泛流行的原因之一。其实,CORBA有一个非常有名的接班人,他就是Facebook的Thrift框架。Thrift还使用一种IDL编译生成多种语言的远程调用方案,完全实现了C++/JAVA等多种语言的通信承载,因此是开源框架中特别吸引人的一个。Thrfit通信承载的另一个特点是它可以结合各种传输协议和编码协议,例如TCP/UDP/HTTP和JSON/BIN/PB……这使得选择几乎任何网络环境成为可能。Thrift的模型类似于下图,其中一些存根代表“存根代码”,是客户端直接使用的功能程序;skeleton代表“骨架代码”,需要程序员编写专门提供远程服务功能的模板代码。模板可以填写或继承(扩展)。这种存根骨架模型几乎是所有远程调用方案的标准。2、JAVARMIJAVARMI是JAVA虚拟机自带的远程调用方案。它还可以使用URL定位远程对象,使用JAVA自带的序列化编码协议传递参数值。在接口描述中,由于这是一个仅限于JAVA环境的解决方案,所以直接使用JAVA语言的Interface类型作为定义语言。用户通过实现该接口类型来提供远程服务,JAVA会根据该接口文件自动生成客户端调用代码供调用者使用。他的底层通信实现仍然是使用TCP协议实现的。这里的Interface文件就是JAVA语言的IDL,也是开发者填写远程服务内容的骨架模板。由于JAVA的反射功能,stub代码直接由虚拟机编排。该方案由于支持JAVA虚拟机,使用起来非常简单,按照logo的JAVA编程方法即可轻松解决问题,但只能在JAVA环境下运行,限制了其适用范围.鱼和熊掌不可兼得,易用性和适用性往往是相互矛盾的。这与CORBA/Thrift追求最大范围的适用性有很大的不同,这也导致了两者在易用性上的差异。3、WindowsRPCWindows对RPC的支持比较早,也比较完善。它首先通过GUID查询对象,然后使用C语言类型作为传递的参数值。由于WindowsAPI主要是C语言,对于RPC功能,还是需要用一个IDL来描述接口,最后生成.h和.c文件来产生RPCstub和skeleton代码。至于通信机制,由于是操作系统自带的,由内核LPC机制承载,对用户来说更方便。但也仅限于用于Windows程序之间的调用。4.WebService&REST在互联网时代,程序之间需要通过互联网相互调用。Internet上最流行的协议是HTTP协议和WWW服务,所以使用HTTP协议的WebService自然成为最流行的跨系统调用方案。由于互联网的大部分基础设施都可以使用,因此WebService的开发和实现几乎没有困难。一般来说,它使用URL来定位远程对象,通过一系列预定义的类型(主要是C语言的基本类型)和对象序列化方法来传递参数。在接口生成方面,可以直接自己解析HTTP,也可以使用WSDL或SOAP等规范。在REST方案中,只有PUT/GET/DELETE/POST这四个操作函数,其他都是参数。总结以上RPC解决方案,我们发现对于远程调用的三个核心问题,一般业界有以下几种选择:远程对象定位:使用URL;或使用名称服务查找远程调用参数传递:使用C基本类型定义;或使用预定的序列化(反序列化)方案接口定义:使用特定的格式技术直接预先约定一个接口定义文件;或者使用某种描述协议IDL生成这些接口文件通信承载:有的使用特定的TCP/UDP服务器,有的允许用户开发定制的通信模型;还有更高级的传输协议选项,例如HTTP或消息队列。在我们确定了远程调用系统之后,经过几种可行方案的选择,自然要理清每种方案的优缺点,从而选择真正符合需求的设计:远程对象说明:使用URL是互联网标准,更方便用户理解,也便于日后添加需要扩展的内容,因为URL本身就是一个由多个部分组成的字符串;虽然名字服务比较老,但是它还是有它的优势的,就是名字服务可以伴随着负载均衡、容灾扩展、自定义路由等一系列的特性,更容易实现复杂的定位要求。远程调用的接口描述:如果限于某种语言、操作系统、平台,直接使用“隐喻”接口描述,或者使用“注解”类型的注解来标记源代码实现远程调用的定义界面。这是最方便的。但是,如果需要兼容编译型语言,比如C/C++,就必须使用某种IDL来生成这些编译型语言的源代码。通信承载:为用户定制的通信模块可以提供最佳的适用性,但也增加了用户使用的复杂性。HTTP/消息队列方式在系统部署、运维、编程等方面都比较简单。缺点是性能和传输特性的定制空间相对较小。分析完核心问题,我们还需要考虑一些适用场景:面向对象还是面向过程:如果只考虑进行面向过程的远程调用,我们只需要定位到“函数”即可。而如果是面向对象,则需要定位“对象”。由于函数是无状态的,它的定位过程可以像名字一样简单,而对象需要动态地找到它的ID或句柄。跨语言或单语言:在单语言解决方案中,头文件或接口定义可以完全用一种语言处理。如果是跨语言,IDL是免不了的。HybridcommunicationhostingorHTTPserverhosting:Hybridhosting或许可以使用TCP/UDP/sharedmemory等底层技术,可以提供最优的性能,但是使用起来一定很麻烦。如果使用HTTP服务器,就很简单了,因为WWW服务的开源软件和库很多,客户端可以用浏览器或者一些JS页面调试。缺点是性能低。假设我们现在要为一个业务逻辑非常多变的领域设计一个远程调用系统,比如企业业务应用领域或者游戏服务器领域,我们大概应该选择如下:使用名称服务来定位远程对象:由于enterpriseservice要求高可用性,查询名称时可以使用名称服务来识别和选择可用性服务对象。J2EE解决方案中的EJB(EnterpriseJavaBean)是按名称提供服务的。使用IDL生成接口定义:由于企业服务或者游戏服务,他们的开发语言可能不统一,或者需要C/C++等高性能编程语言,所以只能使用IDL。使用混合通信承载:虽然企业业务似乎不需要运行在非常复杂的网络下,但不同企业的网络环境可能差异很大,所以做一个通用的系统,最好不厌其烦地提供一个混合通信承载,可以从TCP/UDP等多种协议中选择。
