Java 网络编程 —— RMI 框架
概述
RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都是 Java 程序
RMI 框架采用代理来负责客户与远程对象之间通过 Socket 进行通信的细节。RMI 框架为远程对象分别生成了客户端代理和服务器端代理。位于客户端的代理必被称为存根(Stub),位于服务器端的代理类被称为骨架(Skeleton)
当客户端调用远程对象的一个方法时,实际上是调用本地存根对象的相应方法。存根对象与远程对象具有同样的接口。存根采用一种与平台无关的编码方式,把方法的参数编码为字节序列,这个编码过程被称为参数编组。RMI 主要采用Java 序列化机制进行参数编组。存根把以下请求信息发送给服务器:
- 被访问的远程对象的名字
- 被调用的方法的描述
- 编组后的参数的字节序列
服务器端接收到客户端的请求信息,然后由相应的骨架对象来处理这一请求信息,骨架对象执行以下操作:
- 反编组参数,即把参数的字节序列反编码为参数
- 定位要访问的远程对象
- 调用远程对象的相应方法
- 获取方法调用产生的返回值或者异常,然后对它进行编组
- 把编组后的返回值或者异常发送给客户
客户端的存根接收到服务器发送过来的编组后的返回值或者异常,再对它进行反编组,就得到调用远程方法的返回结果
JDK5.0 之后,RMI 框架会在运行时自动为运程对象生成动态代理类(包括存根和骨架类),从而更彻底地封装了 RMI 框架的实现细节,简化了 RMI 框架的使用方式
创建 RMI 应用
创建一个 RMI 应用包括以下步骤:
- 创建远程接口:继承 java.rmi.Remote 接口
- 创建远程类:实现远程接口
- 创建服务器程序:负责在 RMI 注册器中注册远程对象
- 创建客户程序:负贵定位远程对象,并且调用远程对象的方法
1. 创建远程接口
远程接口中声明了可以被客户程序访问的远程方法,并直接或间接继承 java.rmi.Remote 接口
import java.rmi.*;
public interface HelloService extends Remote {
public String echo(String msg) throws RemoteException;
}
2. 创建远程类
远程类必须实现一个远程接口,此外,为了使远程类的实例变成能为远程客户提供服务的远程对象,可通过以下两种途径之一把它导出为远程对象:
-
使远程类继承 java.rmi.server.UnicastRemoteObjcct 类,并且远程类的构构方法必声明抛出 RemoteException
import java.rmi.*; import java.rmi.server.UnicastRemoteObjoct; public class HelloServlceImpl extends UnicagtRemoteObject implements HelloService { private String name; public HelloServicelmpl(String name) throws RemoteException { this.name = name; } public String echo(String msg) throws RemoteException { System.out.println(name + ":测用echo()方法"); return "echo;" + msg + " from" + name; } }
-
如果一个远程类已经继承了其他类,无法再继承 UnicastRemoteObiect 类,那么可以在构造方法中调用 UnicastRemoteObject 类的静态 expotObject 方法,同样,远程类的构造方法也必须声明抛出 RemoteException
public class HelloServlceImpl extends OtherClass implements HelloService { private String name; public HelloServicelmpl(String name) throws RemoteException { this.name = name; //参数 port 指定监听的端口,如果取值为0,就表示监听任意一个匿名端口 UnicagtRemoteObject.exportobject(this, 0); } public String echo(String msg) throws RemoteException { System.out.println(name + ":测用echo()方法"); return "echo;" + msg + " from" + name; } }
3. 创建服务器程序
RMI 采用一种命名服务机制来使得客户程序可以找到服务器上的一个远程对象,RMI注册器提供这种命名服务。好比电话查询系统,那些希望对外公开联系方式的单位先到查询系统登记,当客户想知道某个单位的联系方式时,只需向查询系统提供单位的名字,查询系统就会返回该单位的联系方式
启动 RMI 注册器有两种方式。一种方式是直接运行 rmiregistry.exe 程序,在 JDK 的安装目录的 bin 子目录下有一个 rmiregistry.exe 程序,它是提供命名服务的注册器程序。尽管 rmiregistry 注册器程序也可以单独运行在一个主机上,但出于安全的原因,通常让 rmiregistry 注册器程序与服务器程序运行在同一个主机上
启动 RMI 注册器的另一种方式是在服务器程序中调用 java.rmiregistry.LocateRegistry 类的静态方法 createRegistry()
//默认的监听路口为1099
Registry registry = LocateRegistry.createRegigtry(1099);
向注册器注册远程对象有三种方式:
//创建远程对象
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);
下例的 SimpleServer 类创建了两个 HelloServicelmpl 远程对象,接着创建并启动 RMI 注册器,然后把两个远程对象注册到 RMI 注册器
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class SimpleServer {
public static void main( String args[]) {
try {
HelloService service1 = new HelloServiceImpl("service1");
HelloService service2 = new HelloServiceImpl("service2");
//创建并启动注册器
Registry registry = LocateRegistry.createRegistry(1099);
//注册远程对象
regigtry.rebind("HelloService1", service1);
regigtry.rebind("HelloService2", service2);
} catch(Exception e) {
e.printStackTrace();
}
}
}
关于向 RMI 注册器注册远程对象,需要注意的是,远程对象即使没有在注册器中注册,也可被远程访问
4. 创建客户程序
下例的 SimpleClient 类先获得远程对象的存根对象,接着调用它的远程方法
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class SimpleClient {
public static void main(String args[]) {
try {
//返回本地主机的RMI注册器对象,参数port指定RMI注册器监听的端口
Registry registry = LocateRegistry.getRegistry(1099);
//查找对象,返回与参数name指定的名字所绑定的对象
//返回的是一个名为"com.sun.proxy.$Proxy0"的动态代理类的实例
HelloService service1 = (HelloService) registry.lookup("HelloService1");
HelloService service2 = (HelloService) registry.lookup("HelloService2");
System.out.println(service1.echo("hello"));
System.out.println(service2.echo("hello"));
}
}
}
远程方法中的参数与返回值传递
当客户端调用服务器端的远程对象的方法时,客户端会向服务器端传递参数,服务器端则会向客户端传递返回值。RMI 规范对参数以及返回值的传递的规定如下所述:
- 只有基本类型的数据、远程对象以及可序列化的对象才可以被作为参数或者返回值进行传递
- 如果参数或返回值是一个远程对象,那么把它的存根对象传递到接收方。也就是说接收方得到的是远程对象的存根对象
- 如果参数或返回值是可序列化对象,那么直接传递该对象的序列化数据。也就是说接收方得到的是发送方的可序列化对象的复制品
- 如果参数或返回值是基本类型的数据,那么直接传递该数据的序列化数据。也就是说,接收方得到的是发送方的基本类型的数据的复制品
分布式垃圾收集
在 Java 虚拟机中,对于一个本地对象,只要不被本地 Java 虚拟机内的任何变量引用,它就会结束生命周期,可以被垃圾回收器回收。而对于一个远程对象,不仅会被本地 Java 虚拟机内的变量引用,还会被远程引用
服务器端的一个远程对象受到三种引用:
- 服务器端的一个本地对象持有它的本地引用
- 这个远程对象已经被注册到 RMI 注册器,可以理解为,RMI 注册器持有它的引用
- 客户端获得了这个远程对象的存根对象,可以理解为,客户端持有它的远程引用
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/