RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都是 Java 程序。
RMI 框架采用代理来负责客户与远程对象之间通过 Socket 进行通信的细节。RMI 框架为远程对象分别生成了客户端代理和服务器端代理。位于客户端的代理必被称为存根(Stub),位于服务器端的代理类被称为骨架(Skeleton)。
当客户端调用远程对象的一个方法时,实际上是调用本地存根对象的相应方法。存根对象与远程对象具有同样的接口。存根采用一种与平台无关的编码方式,把方法的参数编码为字节序列,这个编码过程被称为参数编组。RMI 主要采用Java 序列化机制进行参数编组。
存根把以下请求信息发送给服务器:
服务器端接收到客户端的请求信息,然后由相应的骨架对象来处理这一请求信息,骨架对象执行以下操作:
客户端的存根接收到服务器发送过来的编组后的返回值或者异常,再对它进行反编组,就得到调用远程方法的返回结果
JDK5.0 之后,RMI 框架会在运行时自动为运程对象生成动态代理类(包括存根和骨架类),从而更彻底地封装了 RMI 框架的实现细节,简化了 RMI 框架的使用方式。
方法调用从客户对象-经-存根(stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。
RMI由3个部分构成:
第一个是rmiregistry(JDK提供的一个可以独立运行的程序,在bin目录下)
第二个是server端的程序,对外提供远程对象
第三个是client端的程序,想要调用远程对象的方法。
首先,先启动rmiregistry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)。
其次,server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry(下面实例用的Registry)等类的bind或rebind方法将刚才实例化好的实现类注册到rmiregistry上并对外暴露一个名称。
最后,client端通过本地的接口和一个已知的名称(即rmiregistry暴露出的名称)再使用RMI提供的Naming/Context/Registry等类的lookup方法从RMIService那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,便可以实现远程调用对象的方法了。
Java程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,在分布式系统中,由于对象不存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,那么怎么解决这个问题呢?
第一种:
将引用传递更改为值传递,也就是将对象序列化为字节,然后使用该字节的副本在客户端和服务器之间传递,而且一个虚拟机中对该值的修改不会影响到其他主机中的数据;
但是对象的序列化也有一个问题,就是对象的嵌套引用就会造成序列化的嵌套,这必然会导致数据量的激增,因此我们需要有选择进行序列化。
在Java中一个对象如果能够被序列化,需要满足下面两个条件之一:
–是Java的基本类型;
–实现java.io.Serializable接口(String类即实现了该接口);
对于容器类,如果其中的对象是可以序列化的,那么该容器也是可以序列化的;
可序列化的子类也是可以序列化的;
第二种:
使用引用传递,每当远程主机调用本地主机方法时,该调用还要通过本地主机查询该引用对应的对象,在任何一台机器上的改变都会影响原始主机上的数据,因为这个对象是共享的;
RMI中的参数传递和结果返回可以使用的三种机制(取决于数据类型):
简单类型:按值传递,直接传递数据拷贝;
远程对象引用(实现了Remote接口):以远程对象的引用传递;
远程对象引用(未实现Remote接口):按值传递,通过序列化对象传递副本,本身不允许序列化的对象不允许传递给远程方法;
在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在RMI中是一个关键的问题,如果将远程对象的发现类比于IP地址的发现可能比较好理解一些。
平常我们上网是通过域名来定位一个网站,实际上网络是通过IP地址来定位网站,因此其中就存在一个映射的过程,域名系统(DNS)就是为了这个目的出现的,在域名系统中通过域名来查找对应的IP地址来访问对应的服务器。
对应的,IP地址在这里就相当于远程对象的引用,而DNS则相当于一个注册表(Registry)
而域名在RMI中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL地址格式的,它要满足的规范如下:
该名称是URL形式的,类似于http的URL,schema是rmi;
格式类似于rmi://host:port/name,host指明注册表运行的注解,port表明接收调用的端口,name是一个标识该对象的简单名称
主机和端口都是可选的,如果省略主机,则默认运行在本地;如果端口也省略,则默认端口是1099;
创建一个接口Hello,该接口需要继承Remote接口,接口所定义的方法需要抛出RemoteException异常:
//远程接口,该接口需要继承Remote接口,并且接口中的方法全都要抛出RemoteException异常
public interface Hello extends Remote {
public String welcome(String name) throws RemoteException;
}
基于上面定义的接口实现一个类Helloimpl,该实现类需要继承UnicastRemoteObject类,同样重载的方法需要抛出RemoteException异常:
/**
* 远程接口实现类,必须继承UnicastRemoteObject
* (继承RemoteServer->继承RemoteObject->实现Remote,Serializable),
* 只有继承UnicastRemoteObject类,才表明其可以作为远程对象,被注册到注册表中供客户端远程调用
* (补充:客户端lookup找到的对象,只是该远程对象的Stub(存根对象),
* 而服务端的对象有一个对应的骨架Skeleton(用于接收客户端stub的请求,以及调用真实的对象)对应,
* Stub是远程对象的客户端代理,Skeleton是远程对象的服务端代理,
* 他们之间协作完成客户端与服务器之间的方法调用时的通信。)
*/
public class HelloImpl extends UnicastRemoteObject implements Hello {
//因为UnicastRemoteObject的构造方法抛出了RemoteException异常,
//因此这里默认的构造方法必须写,也必须声明抛出RemoteException异常
public HelloImpl() throws RemoteException {
}
@Override
public String welcome(String name) throws RemoteException {
return "Hello " + name;
}
}
如果一个远程类已经继承了其他类,无法再继承 UnicastRemoteObiect 类,那么可以在构造方法中调用 UnicastRemoteObject 类的静态 expotObject 方法,同样,远程类的构造方法也必须声明抛出 RemoteException
public class HelloImpl2 implements Hello {
@Override
public String welcome(String name) throws RemoteException {
return "Hello " + name;
}
public HelloImpl2() throws RemoteException{
//参数 port 指定监听的端口,如果取值为0,就表示监听任意一个匿名端口
UnicastRemoteObject.exportObject(this, 0);
}
}
服务端创建了一个注册表,并注册了客户端需要的对象:
public class Server {
public static void main(String[] args) throws RemoteException {
//创建对象
Hello hello = new HelloImpl();
// 本地主机上的远程对象注册表Registry的实例,
// 并指定端口,这一步必不可少(Java默认端口是1099)
Registry registry = LocateRegistry.createRegistry(1099);
//绑定对象到注册表,并给它取名为hello
registry.rebind("hello", hello);
}
}
向注册器注册远程对象有三种方式:
//创建远程对象
HelloService service1 = new HelloServiceImpl("service1");
//方式1:调用 java.i.registry.Registy 接口的 bind 或 rebind 方法
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("HelloService1", service1);
//方式2:调用命名服务类 java.rmi.Naming 的 bind 或 rebind 方法
Naming.rebind("HelloService1", service1);
//方式3:调用 JNDI API 的 javax.naming.Context 接口的 bind 或rebind 方法
Context namingContext = new InitialContext();
namingContext.rebind("rmi:HelloService1", service1);
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
//获取到注册表的代理
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
//利用注册表的代理去查询远程注册表中名为hello的对象
Hello hello = (Hello) registry.lookup("hello");
//调用远程方法
System.out.println(hello.welcome("tom"));
}
}
当客户端调用服务器端的远程对象的方法时,客户端会向服务器端传递参数,服务器端则会向客户端传递返回值。RMI 规范对参数以及返回值的传递的规定如下所述:
在 Java 虚拟机中,对于一个本地对象,只要不被本地 Java 虚拟机内的任何变量引用,它就会结束生命周期,可以被垃圾回收器回收。而对于一个远程对象,不仅会被本地 Java 虚拟机内的变量引用,还会被远程引用
服务器端的一个远程对象受到三种引用:
RMI 框架采用分布式垃圾收集机制来管理远程对象的生命周期,当一个远程对象不受到任何本地引用和远程引用时,这个远程对象才会结束生命周期,并且可以被本地 Java 虚拟机的垃圾回收器回收。
服务器端如何知道客户端持有一个远程对象的远程引用呢?当客户端获得了一个服务器端的远程对象的存根后,就会向服务器发送一条租约通知,告诉服务器自己持有这个远程对象的引用了。客户端对这个远程对象有一个租约期限,默认值为 600000ms。当至达了租约期限的一半时间,客户如果还持有远程引用,就会再次向服务器发送租约通知。客户端不断在给定的时间间隔中向服务器发送租约通知,从而使肠务器知道客户端一直持有远程对象的引用。如果在租约到期后,服务器端没有继续收到客户端的新的租约通知,服务器端就会认为这个客户已经不再持有远程对象的引用了
远程对象一般分布在服务器端,当客户端试图调用远程对象的方法时,如果在客户端还不存在远程对象所依赖的类文件,比如远程方法的参数和返回值对应的类文件,客户就会从 java.rmi.server.codebase 系统属性指定的位贸动态加载该类文件
同样,当服务器端访问客户端的远程对象时,如果服务器端不存在相关的类文件,腐务器就会从 java.rmi.server.codebase 属性指定的位置动态加载它们
此外,当服务器向 RMI 注册器注册远程对象时,注册器也会从 java.rmi.server.codebase 属性指定的位置动态加载相关的远程接口的类文件
前面的例子都是在同一个 classpath 下运行服务器程序以及客户程序的,这些程序都能从本地 classpath 中找到相应的类文件,因此无须从 java.rmi.server.codebase 属性指定的位置动态加载类。而在实际应用中,客户程序与服务器程序运行在不同的主机上,因此当客户端调用服务器端的远程对象的方法时,有可能需要从远程文件系统加载类文件。同样,当服务器端调用客户端的远程对象的方法时,也有可能从远程文件系统加载类文件
我们可以且把这些需要被加载的类的文件都集中放在网络上的同一地方,启动时将java.rmi.server.codebase 设置为指定位置,从而实现动态加载
start java -Djava.rmi.server.codebase=http://www.javathinker.net/download/