版权声明:本文由韩伟原创文章,转载请注明出处:
文章原文连接:https://www.qcloud.com/community/article/162程序员
来源:腾云阁 https://www.qcloud.com/community编程
RPC是一种方便的网络通讯编程模型,因为和编程语言的高度结合,大大减小了处理网络数据的复杂度,让代码可读性也有可观的提升。可是RPC自己的构成却比较复杂,因为受到编程语言、网络模型、使用习惯的约束,有大量的妥协和取舍之处。本文就是经过分析几种流行的RPC实现案例,提供你们在设计RPC系统时的参考。浏览器
因为RPC底层的网络开发通常和具体使用环境有关,而编程实现手段也很是多样化,但不影响使用者,所以本文基本涉及如何实现一个RPC系统。服务器
咱们在各类操做系统、编程语言生态圈中,多少都会接触过“远程调用”的概念。通常来讲,他们指的是用简单的一行代码,经过网络调用另一个计算机上的某段程序。好比:网络
RMI——Remote Method Invoke:调用远程的方法。“方法”通常是附属于某个对象上的,因此一般RMI指对在远程的计算机上的某个对象,进行其方法函数的调用。数据结构
RPC——Remote Procedure Call:远程过程调用。指的是对网络上另一个计算机上的,某段特定的函数代码的调用。负载均衡
远程调用自己是网络通讯的一种概念,他的特色是把网络通讯封装成一个相似函数的调用。网络通讯在远程调用外,通常还有其余的几种概念:数据包处理、消息队列、流过滤、资源拉取等待。下面比较一下他们差别:框架
方案 | 编程方式 | 信息封装 | 传输模型 | 典型应用 |
---|---|---|---|---|
远程调用 | 调用函数,输入参数,得到返回值。 | 使用编程语言的变量、类型、函数 | 发出请求,得到响应 | Java RMI |
数据包处理 | 调用Send()/Recv(),使用字节码数据,编解码,处理内容 | 把通讯内容构形成二进制的协议包 | 发送/接收 | UDP编程 |
消息队列 | 调用Put()/Get(),使用“包”对象,处理其包含的内容 | 消息被封装成语言可用的对象或结构 | 对某队列,存入一个消息;取出一个消息 | ActiveMQ |
流过滤 | 读取一个流,或写出一个流,对流中的单元包即刻处理 | 单元长度很小的统一数据结构 | 链接;发送/接收;处理 | 网络视频 |
资源拉取 | 输入一个资源ID,得到资源内容 | 请求或响应都包含:头部+正文 | 请求后等待响应 | WWW |
针对远程调用的特色——调用函数。业界在各类语言下都开发过相似的方案,同时也有些方案是试图作到跨语言的。尽管远程调用在编程方式上,看起来彷佛是最简单易用的,可是也有明显的缺点。因此了解清楚远程调用的优点和缺点,是决定是否要开发、或者使用远程调用这种模型的关键问题。运维
远程调用的优点有:编程语言
屏蔽了网络层。所以在传输协议和编码协议上,咱们能够选择不一样的方案。好比WebService方案就是用的HTTP传输协议+SOAP编码协议;而REST的方案每每使用HTTP+JSON协议。Facebook的Thrift甚至能够定制任何不一样的传输协议和编码协议,你能够用TCP+Google Protocol Buffer,也能够用UDP+JSON……。因为屏蔽了网络层,你能够根据实际须要来独立的优化网络部分,而无需涉及业务逻辑的处理代码,这对于须要在各类网络环境下运行的程序来讲,很是有价值。
函数映射协议。你能够直接用编程语言来书写数据结构和函数定义,取代编写大量的编码协议格式和分包处理逻辑。对于那些业务逻辑很是复杂的系统,好比网络游戏,能够节省大量定义消息格式的时间。并且函数调用模型很是容易学习,不须要学习通讯协议和流程,让经验较浅的程序员也能很容易的开始使用网络编程。
远程调用的缺点:
增长了性能消耗。因为把网络通讯包装成“函数”,须要大量额外的处理。好比须要预生产代码,或者使用反射机制。这些都是额外消耗CPU和内存的操做。并且为了表达复杂的数据类型,好比变长的类型string/map/list,这些都要数据包中增长更多的描述性信息,则会占用更多的网络包长度。
没必要要的复杂化。若是你仅仅是为了某些特定的业务需求,好比传送一个固定的文件,那么你应该用HTTP/FTP协议模型。若是为了作监控或者IM软件,用简单的消息编码收发会更快速高效。若是是为了作代理服务器,用流式的处理会很简单。另外,若是你要作数据广播,那么消息队列会很容易作到,而远程调用这几乎没法完成。
所以,远程调用最适合的场景是:业务需求多变,网络环境多变。
因为远程调用的使用接口是“函数”,因此要如何构建这个“函数”,就产生了三个方面须要决策的问题:
如何表示“远程”的信息。所谓远程,就是指网络上另一个位置,那么网络地址就是必需要输入的部分。在TCP/IP网络下,IP地址和端口号表明了运行中程序的一个入口。因此指定IP地址和端口是发起远程调用所必需的。然而,一个程序可能会运行不少个功能,能够接收多个不一样含义的远程调用。这样如何去让用户指定这些不一样含义的远程调用入口,就成为了另一个问题。固然最简单的是每一个端口一种调用,可是一个IP最多支持65535个端口,并且别的网络功能也可能须要端口,因此这种方案可能会不够用,同时一个数字表明一个功能也不太好理解,必需要查表才能明白。因此咱们必须想别的方法。在面向对象的思想下,有些方案提出了:以不一样的对象来概括不一样的功能组合,先指定对象,再指定方法。这个想法很是符合程序员的理解方式,EJB就是这种方案的。一旦你肯定了用对象这种模型来定义远程调用的地址,那么你就须要有一种指定远程对象的方法,为了指定对象,你必需要能把对象的一些信息,从被调用方(服务器端)传输给调用方(客户端)。最简单的方案就是客户端输入一串字符串做为对象的“名字”,发给服务器端,查找注册了这个“名字”的对象,若是找到了,服务器端就会用某种技术“传输”这个对象给客户端,而后客户端就能够调用他的方法了。固然这种传输不多是把整个服务器上的对象数据拷贝给客户端,而是用一些符号或者标志的方法,来表明这个服务器上的对象,而后发给客户端。若是你不是使用面向对象的模型,那么远程的一个函数,也是必需要定位和传输的,由于你调用的函数必须先能找到,而后成为客户端侧的一个接口,才能调用。针对“远程对象”(这里说的对象包括面向对象的对象或者仅仅是 函数)如何表达才能在网络上定位;以及定位成功以后以什么形式供客户端调用,都是“远程调用”设计方案中第一个重要的问题。
函数的接口形式应该如何表示。远程调用因为受到网络通讯的约束,因此每每不能彻底的支持编程语言的全部特性。好比C语言函数中的指针类型参数,就没法经过网络传递出去。所以远程调用的函数定义,能用语言中的什么特性,不能用什么特性,是须要在设计方案是规定下来的。这种规定若是太严格,会影响使用者的易用性;若是太宽泛,则可能致使远程调用的性能低下。如何去设计一种方式,把编程语言中的函数,描述成一个远程调用的函数,也是须要考虑的问题。不少方案采用了配置文件这种通用的方式,而另一些方案能够直接在源代码中里面加特殊的注释。通常来讲,编译型语言如C/C++只能采用源代码根据配置文件生成的方案,虚拟机型语言如C#/JAVA能够采用反射机制结合配置文件(设置是在源代码中用特殊注释来代替配置文件)的方案,若是是脚本语言就更简单,有时候连配置文件都不须要,由于脚本本身就能够充当。总之远程调用的接口要知足怎样的约束,也是一个须要仔细考虑的问题。
用什么方法来实现网络通讯。远程调用最重要的实现细节,就是关于网络通讯。用何种通讯方式来承载远程调用的问题,细化下来就是两个子问题:用什么样的服务程序提供网络功能?用什么样的通讯协议?远程调用系统能够本身直接对TCP/IP编程来实现通讯,也能够委托一些其余软件,好比Web服务器、消息队列服务器等等……也可使用不一样的网络通讯框架,如Netty/Mina这些开源框架。通讯协议则通常有两层:一个是传输协议,好比TCP/UDP或者高层一点的HTTP,或者本身定义的传输协议;另一个是编码协议,就是如何把一个编程语言中的对象,序列化和反序列化成为二进制字节流的方案,流行的方案有JSON、Google Protocol Buffer等等,不少开发语言也有本身的序列化方案,如JAVA/C#都自带。以上这些技术细节,应该选择使用哪些,直接关系到远程调用系统的性能和环境兼容性。
以上三个问题,就是远程调用系统必须考虑的核心选型。根据每一个方案所面对的约束不一样,他们都会在这三个问题上作出取舍,从而适应其约束。可是如今并不存在一个“万能”或者“通用”的方案,其缘由就是:在如此复杂的一个系统中,若是要照顾的特性越多,须要付出的成本(易用性代价、性能开销)也会越多。下面,咱们能够研究下业界现存的各类远程调用方案,看他们是如何在这三个方面作平衡和选择的。
业界方案举例:
JAVA RMI是JAVA虚拟机自带的一个远程调用方案。它也是可使用URL来定位远程对象,使用JAVA自带的序列化编码协议传递参数值。在接口描述上,因为这是一个仅限于JAVA环境下的方案,因此直接用JAVA语言的Interface类型做为定义语言。用户经过实现这个接口类型来提供远程服务,同时JAVA会根据这个接口文件自动生成客户端的调用代码供调用者使用。他的底层通讯实现,仍是用TCP协议实现的。在这里,Interface文件就是JAVA语言的IDL,同时也是skeleton模板,供开发者来填写远程服务内容。而stub代码则因为JAVA的反射功能,由虚拟机直接包办了。这个方案因为JAVA虚拟机的支持,使用起来很是简单,彻底按照标志的JAVA编程方法就能够轻松解决问题,可是这也仅仅能在JAVA环境下运行,限制了其适用的范围。鱼与熊掌不可兼得,易用性和适用性每每是互相冲突的。这和CORBA/Thrift追求最大范围的适用性有很大的差异,也致使了二者在易用性上的不一样。
Windows RPC:Windows中对RPC支持是比较早和比较完善的。首先它经过GUID来查询对象,而后使用C语言类型做为参数值的传递。因为Windows的API主要是C语言的,因此对于RPC功能来讲,仍是要用一种IDL来描述接口,最后生成.h和.c文件来生产RPC的stub和skeleton代码。而通讯机制,因为是操做系统自带的,因此使用内核LPC机制承载,这一点仍是对使用者来讲比较方便的。可是也限制了只能用于Windows程序之间作调用。
WebService & REST:在互联网时代,程序须要经过互联网来互相调用。而互联网上最流行的协议是HTTP协议和WWW服务,所以使用HTTP协议的Web Service就瓜熟蒂落的成为跨系统调用的最流行方案。因为可使用大多数互联网的基础设施,因此Web Service的开发和实现几乎是毫无难度的。通常来讲,它都会使用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
混合式通讯承载仍是使用HTTP服务器承载:混合式承载可能能够用到TCP/UDP/共享内存等底层技术,能够提供最优的性能,可是使用起来必然很是麻烦。使用HTTP服务器的话,则很是简单,由于WWW服务的开源软件、库众多,并且客户端使用浏览器或者一些JS页面便可调试,缺点是其性能较低。
假设咱们如今要为某种业务逻辑很是多变的领域,如企业业务应用领域,或游戏服务器端领域,去设计一个远程调用系统,咱们可能应该以下选择:
使用名字服务定位远程对象:因为企业服务是须要高可用性的,使用名字服务能在查询名字时识别和选择可用性服务对象。J2EE方案中的EJB(企业JavaBean)就是用名字服务的。
使用IDL来生成接口定义:因为企业服务或游戏服务,其开发语言可能不是统一的,又或者须要高性能的编程语言如C/C++,因此只能使用IDL。
使用混合式通讯承载:虽然企业服务看起来无需在很复杂的网络下运行,可是不一样的企业的网络环境又多是千差万别的,因此要作一个通用的系统,最好仍是不怕麻烦提供混合式的通讯承载,这样能够在TCP/UDP等各类协议中选择。