在现在的分布式系统中,一种较为成熟的通信方式是:远程过程调用(RPC),在面向对象的系统中,也常常称为远程方法调用(RMI)。后文中我不再区分这两个名字。
远程方法调用具有良好的distribution transparency。在这种模型下,一个进程访问一个远程服务(由另一个进程提供的服务,服务进程可能位于网络中的任何一台计算机)就好像调用了一个本地方法一样;对于服务端而言也是类似的,一个服务提供一个(一组)方法接口,在它看来,它的客户好像在同一个进程中,直接通过这些接口访问相应的服务。
对于RPC,我们应该已经相当熟悉了。java标准库中就有对这种IPC的支持,这部分主要集中在java.rmi包中。在Android系统中,广泛使用了RPC。当然,android系统中的rpc只能用于同一个os中的进程间的通信,因为它的底层是通过binder实现的,而不是某种网络通信方式。
一个RPC系统,为了方便编程,一般会定义一种“接口定义”语言(IDL),例如,对于andrid而言,就是AIDL。这种接口定义语言一般用于定义RPC中远程方法的原型,如名字、参数及其类型、返回值类型、可能异常等。通过这种语言声明某个服务的一组接口(也可能包含一些常量),也就是该服务的接口定义。RPC系统通过接口定义,会自动地生成一些框架代码,最重要的包括client stub,server stub和“头文件”(这里的头文件是广义的,指的是客户端和服务端编程需要的类型声明、库等)。client stub实现了IDL中声明的所有方法和类型。如果客户端代码需要请求服务,则直接调用client stub的方法就可以了,client stub的相应方法会返回正确的结果。
一次RPC请求过程如下:
- 客户端代码获得client stub的一个实例,并调用该实例的服务方法hello();
- client stub的服务函数会创建一个消息,该消息包括调用的方法名、参数等相关信息;
- client stub将创建好的消息通过OS的网络接口发送给服务器主机,阻塞等待服务器主机返回结果;
- server stub接收到服务请求的消息,取出消息中的方法名、调用参数,实际调用服务端该方法的实现;
- 服务端该方法的实际实现完成相应的服务请求,并将结果返回给server stub,在服务端实现看来,这完全是一次本地的调用;
- server stub创建一个响应消息,并将结果放在这个消息中,通过OS的网络接口,发送给客户主机;
- client stub获得响应消息,从中取得结果,并以方法hello()的返回值的方式返回给调用该方法的客户端代码;
- 客户端代码继续执行,就好像调用了一个本地的方法一样。
在RPC的实现中,遇到的主要问题包括:参数/返回值/异常传递和服务定位。
参数/返回值/异常传递,本质上就是参数传递。对于一个方法的参数,可能有三种情况:in、out和in and out。
对于in参数,调用者主要是将相关的信息传递给方法,而不关心方法调用后in参数的值(或者要求方法调用后,in参数的值与调用之前完全一样)。这种情况的处理通常是将参数的值复制一份(如果有引用的话,需要深度复制),通过消息传递给服务端代码。当然,如果需要传递的数据量非常大,并且对数据的处理相对简单,也可能采用另一种方式:仅仅传递这个参数的一个标识符,服务端在完成请求的时候,记录下对这个数据的操作命令,并将这些操作命令传回给客户端,在客户端完成对数据的实际操作。(这也是代码迁移的一种应用,可以认为,服务端迁移了一些代码到客户端执行。)
对于out参数,主要是将信息从方法内传递给调用者,调用者和方法通常都不关心调用之前该参数内部的值。对于这种情况,client stub不需要传递该参数的内容给server stub。server stub自己构造一个参数并调用服务方法,之后,server stub将这个参数的结果值通过消息返回给client stub,client stub在用消息中的值填充响应的参数即可。
最后,对于in and out的参数,则需要结合上面两种方式来传递,代价是最大的。一般而言,在IDL中通常都会定义关键字,让程序员指明某个参数是in还是out还是in and out,以采用对应的传递方法。如果不能明确说明一个参数是in或者是out,那么RPC系统只能保守地将它当作in and out来传递。
第二个问题就是服务定位。client stub怎么知道要将服务请求消息发送给谁?一般而言,在一个RPC系统中,都会有一个特殊的服务,称为目录服务或者名字服务。这个服务有一个公开的域名和端口,并且一直保持运行。当某个服务提供者启动时,都会首先向这个名字服务注册自己。例如,如果某个进程提供天气预报的服务,在它启动时,会主动想名字服务注册“com/lk/WeatherReportService”,名字服务会记录下这个逻辑服务名以及提供这个服务的地址、端口等信息。当client stub被创建时,它会首先向名字服务查询提供相应服务的服务器的地址信息,后续,将服务请求消息发送给这个查出来的地址。在Android平台上,这个名字服务是ServiceManagerService,在Java RMI中,这个服务可以是rmiregistry。
从语义上来看,RPC天生就是同步的。但其实,也完全可以实现成异步的。在异步情况下,方法调用的结果并不是通过返回值返回的,方法的返回值一般是void,而是通过回调函数或者监听器(Listener)返回的。
阅读(1796) | 评论(0) | 转发(0) |