此处讲的是Java
中的RMI
,而不是通用意义上的RMI
,关于通用的RMI
可以参考分布式之RPC的协议以及错误处理 这篇文章。
一、Java RMI
简介
Java RMI
用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上;一个虚拟机中的对象调用另一个虚拟上中的对象的方法,只不过是允许被远程调用的对象要通过一些标志加以标识。这样做的特点如下:
- 优点:避免重复造轮子;
- 缺点:调用过程很慢,而且该过程是不可靠的,容易发生不可预料的错误,比如网络错误等;
在RMI
中的核心是远程对象(remote object),除了对象本身所在的虚拟机,其他虚拟机也可以调用此对象的方法,而且这些虚拟机可以不在同一个主机上。每个远程对象都要实现一个或者多个远程接口来标识自己,声明了可以被外部系统或者应用调用的方法(当然也有一些方法是不想让人访问的)。
1.1 RMI
的通信模型
从方法调用角度来看,RMI
要解决的问题,是让客户端对远程方法的调用可以相当于对本地方法的调用而屏蔽其中关于远程通信的内容,即使在远程上,也和在本地上是一样的。
从客户端-服务器模型来看,客户端程序直接调用服务端,两者之间是通过JRMP
( Java Remote Method Protocol)协议通信,这个协议类似于HTTP协议,规定了客户端和服务端通信要满足的规范。
但是实际上,客户端只与代表远程主机中对象的Stub
对象进行通信,丝毫不知道Server
的存在。客户端只是调用Stub
对象中的本地方法,Stub
对象是一个本地对象,它实现了远程对象向外暴露的接口,也就是说它的方法和远程对象暴露的方法的签名是相同的。客户端认为它是调用远程对象的方法,实际上是调用Stub
对象中的方法。可以理解为Stub
对象是远程对象在本地的一个代理,当客户端调用方法的时候,Stub
对象会将调用通过网络传递给远程对象。
在java 1.2
之前,与Stub
对象直接对话的是Skeleton
对象,在Stub
对象将调用传递给Skeleton
的过程中,其实这个过程是通过JRMP
协议实现转化的,通过这个协议将调用从一个虚拟机转到另一个虚拟机。在Java 1.2
之后,与Stub
对象直接对话的是Server
程序,不再是Skeleton
对象了。
所以从逻辑上来看,数据是在Client
和Server
之间横向流动的,但是实际上是从Client
到Stub
,然后从Skeleton
到Server
这样纵向流动的。
1.2 重要的问题
1.2.1 数据的传递问题
我们都知道在Java
程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,但是对于分布式系统中,由于对象不再存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,那么怎么解决这个问题呢?
- 第一种:将引用传递更改为值传递,也就是将对象序列化为字节,然后使用该字节的副本在客户端和服务器之间传递,而且一个虚拟机中对该值的修改不会影响到其他主机中的数据;但是对象的序列化也有一个问题,就是对象的嵌套引用就会造成序列化的嵌套,这必然会导致数据量的激增,因此我们需要有选择进行序列化,在
Java
中一个对象如果能够被序列化,需要满足下面两个条件之一:
- 是
Java
的基本类型; - 实现
java.io.Serializable
接口(String
类即实现了该接口); - 对于容器类,如果其中的对象是可以序列化的,那么该容器也是可以序列化的;
- 可序列化的子类也是可以序列化的;
- 是
- 第二种:仍然使用引用传递,每当远程主机调用本地主机方法时,该调用还要通过本地主机查询该引用对应的对象,在任何一台机器上的改变都会影响原始主机上的数据,因为这个对象是共享的;
RMI
中的参数传递和结果返回可以使用的三种机制(取决于数据类型):
- 简单类型:按值传递,直接传递数据拷贝;
- 远程对象引用(实现了
Remote
接口):以远程对象的引用传递; - 远程对象引用(未实现
Remote
接口):按值传递,通过序列化对象传递副本,本身不允许序列化的对象不允许传递给远程方法;
1.2.2 远程对象的发现问题
在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在RMI
中是一个关键的问题,如果将远程对象的发现类比于IP
地址的发现可能比较好理解一些。
在我们日常使用网络时,基本上都是通过域名来定位一个网站,但是实际上网络是通过IP
地址来定位网站的,因此其中就需要一个映射的过程,域名系统(DNS
)就是为了这个目的出现的,在域名系统中通过域名来查找对应的IP
地址来访问对应的服务器。那么对应的,IP
地址在这里就相当于远程对象的引用,而DNS
则相当于一个注册表(Registry)。而域名在RMI中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL
地址格