如何设计RPC框架

一.RPC简介

RPCRemote Procedure Call Protocol——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCPUDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层应用层RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

RPC是指远程过程调用,也就是说两台服务器AB,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
比如说,一个方法可能是这样定义的:
Employee getEmployeeByName(String fullName)
那么:

首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。

第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。

第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。

第四,B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。

第五,返回值还要发送回服务器A上的应用,也要经过序列化的方式发送,服务器A接到后,再反序列化,恢复为内存中的表达方式,交给A服务器上的应用




为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用.RPC的协议有很多,比如最早的CORBAJava RMIWeb ServiceRPC风格,HessianThrift,甚至Rest API

 

二.RPC方案的核心问题

由于远程调用的使用接口是函数,所以要如何构建这个函数,就产生了三个方面需要决策的问题:

1 . 如何表示远程的信息

所谓远程,就是指网络上另外一个位置,那么网络地址就是必须要输入的部分。在TCP/IP网络下,IP地址和端口号代表了运行中程序的一个入口。所以指定IP地址和端口是发起远程调用所必需的。

然而,一个程序可能会运行很多个功能,可以接收多个不同含义的远程调用。这样如何去让用户指定这些不同含义的远程调用入口,就成为了另外一个问题。当然最简单的是每个端口一种调用,但是一个IP最多支持65535个端口,而且别的网络功能也可能需要端口,所以这种方案可能会不够用,同时一个数字代表一个功能也不太好理解,必须要查表才能明白。

所以我们必须想别的方法。在面向对象的思想下,有些方案提出了:以不同的对象来归纳不同的功能组合,先指定对象,再指定方法。这个想法非常符合程序员的理解方式,EJB就是这种方案的。一旦你确定了用对象这种模型来定义远程调用的地址,那么你就需要有一种指定远程对象的方法,为了指定对象,你必须要能把对象的一些信息,从被调用方(服务器端)传输给调用方(客户端)。

最简单的方案就是客户端输入一串字符串作为对象的名字,发给服务器端,查找注册了这个名字的对象,如果找到了,服务器端就会用某种技术传输这个对象给客户端,然后客户端就可以调用他的方法了。当然这种传输不可能是把整个服务器上的对象数据拷贝给客户端,而是用一些符号或者标志的方法,来代表这个服务器上的对象,然后发给客户端。

如果你不是使用面向对象的模型,那么远程的一个函数,也是必须要定位和传输的,因为你调用的函数必须先能找到,然后成为客户端侧的一个接口,才能调用。针对远程对象(这里说的对象包括面向对象的对象或者仅仅是 函数)如何表达才能在网络上定位;以及定位成功之后以什么形式供客户端调用,都是远程调用设计方案中第一个重要的问题。

2 . 函数的接口形式应该如何表示

远程调用由于受到网络通信的约束,所以往往不能完全的支持编程语言的所有特性。比如C语言函数中的指针类型参数,就无法通过网络传递出去。因此远程调用的函数定义,能用语言中的什么特性,不能用什么特性,是需要在设计方案是规定下来的。

这种规定如果太严格,会影响使用者的易用性;如果太宽泛,则可能导致远程调用的性能低下。如何去设计一种方式,把编程语言中的函数,描述成一个远程调用的函数,也是需要考虑的问题。很多方案采用了配置文件这种通用的方式,而另外一些方案可以直接在源代码中里面加特殊的注释。

一般来说,编译型语言如C/C++只能采用源代码根据配置文件生成的方案,虚拟机型语言如C#/JAVA可以采用反射机制结合配置文件(设置是在源代码中用特殊注释来代替配置文件)的方案,如果是脚本语言就更简单,有时候连配置文件都不需要,因为脚本自己就可以充当。总之远程调用的接口要满足怎样的约束,也是一个需要仔细考虑的问题。

3. 用什么方法来实现网络通信

远程调用最重要的实现细节,就是关于网络通信。用何种通信方式来承载远程调用的问题,细化下来就是两个子问题:用什么样的服务程序提供网络功能?用什么样的通信协议?

远程调用系统可以自己直接对TCP/IP编程来实现通信,也可以委托一些其他软件,比如Web服务器、消息队列服务器等等……也可以使用不同的网络通信框架,如Netty/Mina这些开源框架。通信协议则一般有两层:一个是传输协议,比如TCP/UDP或者高层一点的HTTP,或者自己定义的传输协议;另外一个是编码协议,就是如何把一个编程语言中的对象,序列化和反序列化成为二进制字节流的方案,流行的方案有JSONGoogle Protocol Buffer等等,很多开发语言也有自己的序列化方案,如JAVA/C#都自带。以上这些技术细节,应该选择使用哪些,直接关系到远程调用系统的性能和环境兼容性。

 

以上三个问题,就是远程调用系统必须考虑的核心选型。根据每个方案所面对的约束不同,他们都会在这三个问题上做出取舍,从而适应其约束。但是现在并不存在一个万能或者通用的方案,其原因就是:在如此复杂的一个系统中,如果要照顾的特性越多,需要付出的成本(易用性代价、性能开销)也会越多。

下面,我们可以研究下业界现存的各种远程调用方案,看他们是如何在这三个方面做平衡和选择的。

针对远程调用的三个核心问题,一般业界有以下几个选择:

远程对象定位:使用URL;或者使用名字服务来查找

远程调用参数传递:使用C的基本类型定义;或者使用某种预订的序列化(反序列化)方案

接口定义:使用某种特定格式的技术,直接按预先约定一种接口定义文件;或者使用某种描述协议IDL来生成这些接口文件

通信承载:有使用特定TCP/UDP之类的服务器,也有可以让用户自己开发定制的通信模型;还有使用HTTP或者消息队列这一类更加高级的传输协议

 

三.方案选型

在我们确定了远程调用系统方案几个可行选择后,自然就要明确一下各个方案的优缺点,这样才能选择真正合适需求的设计:

1. 对于远程对象的描述:使用URL是互联网通行的标准,比较方便用户理解,也容易添加日后需要扩展到内容,因为URL本身是一个由多个部分组合的字符串;而名字服务则老式一些,但是依然有他的好处,就是名字服务可以附带负载均衡、容灾扩容、自定义路由等一系列特性,对于需求复杂的定位比较容易实现。

2. 远程调用的接口描述:如果只限制于某个语言、操作系统、平台上,直接利用隐喻方式的接口描述,或者以注解类型注释手段来标注源代码,实现远程调用接口的定义,是最方便不过的。但是,如果需要兼容编译型语言,如C/C++,就一定要用某种IDL来生成这些编译语言的源代码了。

3.通信承载:给用户自己定制通信模块,能提供最好的适用性,但是也让用户增加了使用的复杂程度。而HTTP/消息队列这种承载方式,在系统的部署、运维、编程上都会比较简单,缺点就是对于性能、传输特性的定制空间就比较小。

分析完核心问题,我们还需要考虑一些适用性场景:

1. 面向对象还是面向过程:如果我们只是考虑做面向过程的远程调用,只需要定位到函数即可。而如果是面向对象的,则需要定位到对象。由于函数是无状态的,所以其定位过程可以简单到一个名字即可,而对象则需要动态的查找到其ID或句柄。

2.跨语言还是单一语言:单一语言的方案中,头文件或接口定义完全用一种语言处理即可,如果是跨语言的,就少不免要IDL

3. 混合式通信承载还是使用HTTP服务器承载:混合式承载可能可以用到TCP/UDP/共享内存等底层技术,可以提供最优的性能,但是使用起来必然非常麻烦。使用HTTP服务器的话,则非常简单,因为WWW服务的开源软件、库众多,而且客户端使用浏览器或者一些JS页面即可调试,缺点是其性能较低。

假设我们现在要为某种业务逻辑非常多变的领域,如企业业务应用领域,或游戏服务器端领域,去设计一个远程调用系统,我们可能应该如下选择:

1. 使用名字服务定位远程对象:由于企业服务是需要高可用性的,使用名字服务能在查询名字时识别和选择可用性服务对象。J2EE方案中的EJB(企业JavaBean)就是用名字服务的。

2. 使用IDL来生成接口定义:由于企业服务或游戏服务,其开发语言可能不是统一的,又或者需要高性能的编程语言如C/C++,所以只能使用IDL

3.使用混合式通信承载:虽然企业服务看起来无需在很复杂的网络下运行,但是不同的企业的网络环境又可能是千差万别的,所以要做一个通用的系统,最好还是不怕麻烦提供混合式的通信承载,这样可以在TCP/UDP等各种协议中选择。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值