第一、RMI简介
The Java Remote Method Invocation (RMI) 全称是java远程方法调用,它允许一个运行在Java虚拟机上的对象调用运行在另一个Java虚拟机对象的方法,RMI提供两个用java语言编写的程序之间的远程交流。
RMI应用通常包含两个独立的程序:客户端和服务端,RMI负责这两端之间的通信。其主体流程是:服务端创建一个或多个可供访问的远程对象,注册到RMI中然后等待客户端的调用;客户端首先获取可访问的远程对象的远程引用,并调用其方法, 注册过程具体来说,服务端调用注册组件为每一个远程对象绑定一个名字,客户端通过这个名字来访问服务端的注册表获取远程对象并调用远程对象的方法。 这样的应用通常也被称为分布式对象应用。 分布式应用通常包含以下几点:
- 定位远程对象
可以通过两种方式来获取远程对象的引用,一个是通过RMI的简单命名功能来注册远程对象;另一个则是作为正常操作的一部分,应用可以传递或者返回一个远程对象的引用 。
- 与远程对象交流
与远程对象之间的交流主要是RMI来完成,对于程序员来说,交流过程就跟普通的调用方法一样简单。
- 加载作为传递参数或者作为返回值的对象的字节码
由于RMI允许调用者传递对象到远程对象,因此RMI提供了加载对象代码的机制 。
第二、RMI术语
RMI使用在RPC系统中应用的标准机制来与远程对象交互:stub和skeleton。下面分别介绍一下这两个对象:
1、stub
stub表示远程对象的存根,在客户端中被用来当做远程对象的当地引用或者远程对象的代理。这个stub实现了与远程对象一样的接口,客户端调用stub上的方法时,stub会负责执行远程对象上同名方法的调用。当调用stub上的方法时会做以下几件事:
- 初始化与包含远程对象的JVM的连接
- 传递(marshal)参数到远程JVM
- 等待远程JVM上方法调用的结果
- 读取(unmarshal)返回的值或者返回的异常
- 返回值给调用者
2、skeleton
在远程JVM,每一个远程对象可能都会有一个skeleton,他负责分派接收到的方法调用到实际的远程对象的实现。当skeleton接收到一个方法调用会做以下几件事:
- 为远程方法读取(unmarshal)参数
- 调用实际的远程对象的方法
- 传递(marshal)结果(值或者异常)到调用者
注意 :在java2中,stub和skeleton是由rmic编译器自动生成的。
通过对术语的讲解,这样用图来展示RMI的执行过程就比较明白了,看下图:
第三、例子
一个对象如果要成为一个远程对象,必须实现一个远程接口,远程接口具有的特征是:
- 必须继承java.rmi.Remote接口,这个接口是一个标记接口,没有任何的方法
- 在接口中声明的每一个方法都至少需要声明抛出java.rmi.RemoteException异常。
下面一步步创建一个简单的例子。
这个例子的处理流程是:客户端提交一个任务(一个接口)给服务端,服务端执行这个任务,然后将结果返回给客户端。
步骤1、编写服务端
- 定义远程接口
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
<T> T executeTask(Task<T> t) throws RemoteException;
}
通过继承Remote接口,表明接口Computer内的方法是可以北远程调用的,并且任何实现了这个接口的对象都是一个远程对象。其内部的方法声明抛出了RemoteException异常,这个异常是一个受检查型异常,因此调用这个方法的地方要捕获这个异常。由于远程接口中定义了一个Task的接口,因此这里需要定义这个Task接口:
package compute;
public interface Task<T> {
T execute();
}
Task接口只是一个普通的含有一个方法的接口,定义了一个类型参数,表明运行结果对象的类型。由于RMI通过java对象的序列化机制来在两个JVM之间传递对象,因此实现Task接口的对象也必须实现java.io.Serializable接口,这样才能在客户端调用远程接口。
- 实现远程接口
注意:实现远程接口的对象一般需要做以下几件事:
1、声明实现远程接口
2、为每一个远程对象定义一个构造函数
3、实现远程接口声明的方法
一个服务端需要创建一个远程对象并把这个对象暴露给客户端,并且由于这个例子需要用到RMI的动态下载代码功能(每一个客户端调用Compute 的方法都需要传递一个Task的对象,然后服务端对动态的下载这个task实现对象的代码到服务端JVM,然后运行),因此需要创建和安装java的安全管理。服务器端需要做以下几件事:
1、创建安装安全管理
2、创建并且导出一个或者多个远程对象
3、利用RMI注册或者命名服务注册一个或者多个远程对象
下面是服务端的代码实现,这个类既作为服务端又作为远程对象的实现:
package engine;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;
public class ComputeEngine implements Compute {
public ComputeEngine() {
super();
}
public <T> T executeTask(Task<T> t) {
return t.execute();
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine, 0);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}
介绍一下RMI服务端main方法的实现:
首先创建并安装安全管理。这能够阻止不信任的 下载 代码访问系统资源 ,安全管理能够决定下载的代码是否具有访问服务器资源或者执行一些其他的需要特权的操作。如果不安装安全管理,则服务端不能进行动态下载代码操作,这个例子也就运行不成功,安装安全管理的代码就是这两句:
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
然后实例化远程对象并将其暴露给客户端。UnicastRemoteObject的静态方法 exportObject 是用来暴露远程对象的,第一个参数是原创对象的实例,第二个参数是一个int值,用来指定需要监听的TCP的端口,可以是0,表示是匿名端口,实际端口有RMI在运行时决定或者由操作系统决定。 exportObject 方法返回的对象是一个远程对象的存根(stub),stub实际上是远程对象的一个代理实现,与远程对象实现了相同的接口,需要注意的是返回的对象必须是用接口来命名,而不能用接口的实现类来命名。代码如下:
Compute engine = new ComputeEngine();
Compute stub = (Compute) UnicastRemoteObject.exportObject(engine, 0);
其次绑定远程对象存根,以供客户端获取远程对象的stub,代码如下:
Registry registry = LocateRegistry.getRegistry();
registry.rebind("Compute", stub);
无参数的getRegistry()方法默认是在当前主机默认端口为1099下创建一个Registry对象,如果想指定别的端口,需要调用getRegistry()的重载方法来指定端口。registry.rebind("compute",stub)表明,如果客户端通过Registry获取远程对象,将会获取到一个远程对象的stub的拷贝,因此远程对象是引用传递,而不是值传递。
注意: 出于安全原因,一个远程对象的引用只能在同一个主机下被 bind
, unbind
, or rebind
步骤2、编写客户端
- 实现Task接口
由于Task是需要向服务端传递的参数,因此需要序列化Task的接口实现。代码如下:
package client;
import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;
public class Pi implements Task<BigDecimal>, Serializable {
private static final long serialVersionUID = 227L;
/** constants used in pi computation */
private static final BigDecimal FOUR =
BigDecimal.valueOf(4);
/** rounding mode to use during pi computation */
private static final int roundingMode =
BigDecimal.ROUND_HALF_EVEN;
/** digits of precision after the decimal point */
private final int digits;
/**
* Construct a task to calculate pi to the specified
* precision.
*/
public Pi(int digits) {
this.digits = digits;
}
/**
* Calculate pi.
*/
public BigDecimal execute() {
return computePi(digits);
}
/**
* Compute the value of pi to the specified number of
* digits after the decimal point. The value is
* computed using Machin's formula:
*
* pi/4 = 4*arctan(1/5) - arctan(1/239)
*
* and a power series expansion of arctan(x) to
* sufficient precision.
*/
public static BigDecimal computePi(int digits) {
int scale = digits + 5;
BigDecimal arctan1_5 = arctan(5, scale);
BigDecimal arctan1_239 = arctan(239, scale);
BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
arctan1_239).multiply(FOUR);
return pi.setScale(digits,
BigDecimal.ROUND_HALF_UP);
}
/**
* Compute the value, in radians, of the arctangent of
* the inverse of the supplied integer to the specified
* number of digits after the decimal point. The value
* is computed using the power series expansion for the
* arc tangent:
*
* arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 +
* (x^9)/9 ...
*/
public static BigDecimal arctan(int inverseX,
int scale)
{
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf(inverseX);
BigDecimal invX2 =
BigDecimal.valueOf(inverseX * inverseX);
numer = BigDecimal.ONE.divide(invX,
scale, roundingMode);
result = numer;
int i = 1;
do {
numer =
numer.divide(invX2, scale, roundingMode);
int denom = 2 * i + 1;
term =
numer.divide(BigDecimal.valueOf(denom),
scale, roundingMode);
if ((i % 2) != 0) {
result = result.subtract(term);
} else {
result = result.add(term);
}
i++;
} while (term.compareTo(BigDecimal.ZERO) != 0);
return result;
}
}
注意: Task接口的实现同时实现了Serializable接口,表示序列化对象,在接口实现内部,定义了一个serialVersionUID
对象,之所以增加这个对象,主要是因为Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的,增加这个对象主要是为了反序列化时能保持版本一致性。如果不定义这个属性,java会根据编译后的字节码生成一个 serialVersionUID,这样当修改了这个类时再反序列化修改之前序列化的内容时,就会出现版本不一致(因为java会为修改了的类再生成一个 serialVersionUID,这两个版本的 serialVersionUID肯定是不一样的 )而出错。
- 实现客户端
客户端代码如下:
package client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.math.BigDecimal;
import compute.Compute;
public class ComputePi {
public static void main(String args[]) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Registry registry = LocateRegistry.getRegistry(args[0]);
Compute comp = (Compute) registry.lookup(name);
Pi task = new Pi(Integer.parseInt(args[1]));
BigDecimal pi = comp.executeTask(task);
System.out.println(pi);
} catch (Exception e) {
System.err.println("ComputePi exception:");
e.printStackTrace();
}
}
}
LocateRegistry.getRegistry()方法来获取Registry对象, getRegistry(“主机地址”)表示获取在主机ip下默认端口1099下的Registry,如果不是用的默认端口,则调用 getRegistry的重载方法。
步骤3、运行
- 首先启动Java RMI Registry
在dos下执行命令:start rmiregistry [如果不用默认端口,则后面加端口],这里需要注意的是必须在服务端编译后的字节码文件的最上层包所在的目录启动,不然会报找不到类的异常。
- 运行server
- 运行client
第四、RMI的参数传递
RMI客户端与服务端之间可传递的参数有下列三种类型:原始类型、远程对象、实现了Serializable接口的当地对象。任何不属于这三种类型的对象不能给传递。至于参数是怎样传递的,遵循如下规则:
- 远程对象传递引用。这意味如果客户端调用远程对象的方法改变了远程对象的状态,则会影响到原始的远程对象的状态。当远程对象传递时,只能调用远程对象所实现的远程接口的方法 。
- 其余对象按照对象的序列号拷贝一份传递。这种对象除了标记为static 或者transient的对象不会被拷贝,其余的都会被拷贝。由于是按照拷贝传递,因此所有的对拷贝对象的状态修改不会影响到原对象。
第五、 RMI与线程
RMI不会保证 会为每一个 客户端调用的远程对象分配到一个线程中,因此不同客户端对于同一个远程对象的方法调用可能会同时进行,因此这就要求远程对象的实现要保证是线程安全的。
-------------------------------------------------------参考文献-------------------------------------------------------------------------------------------