RMI(Remote Method Invocation):远程方法调用,允许一个JVM上的程序调用另一个JVM上运行的对象中的方法,即程序的一部分运行于本地计算机,而其他部分运行于远程主机。基于RMI,不同主机上的Java 对象能够类似于相同虚拟机上运行的对象,以一种相似的方式通信:即调用对象中的方法。远程对象存在于服务器上,每个远程对象都实现了一个远程接口,它会指明哪些方法可以有客户端调用。客户端调用远程对象的方法几乎与调用本地方法相同,只不过RMI试图在最大程度上隐藏本地和远程方法调用之间的区别。
远程对象和本地对象之间的区别在于:远程对象驻留在不同虚拟机中。正常情况下,要向方法传递参数,以及方法要返回对象值,这是通过引用某个虚拟机中的内容完成的。这称为“传引用”,但是,调用方法和被调用方法在不同的虚拟机中,这种方法就不适用了。
为了向远程方法传递参数和从远程方法返回结果,可以使用三种方式,这取决于所传递参数的类型。
- 简单类型(int、boolean、double等)是按值传递的,这与调用本地方法一样。
- 远程对象的引用(即实现了Remote接口的对象)是已远程引用传递的,即允许接收方调用远程对象上的方法。这与向本地方法传递对象应用的方式相似。
- 没有实现Remote接口的对象是按值传递的,该对象需实现Serializable接口,即通过对象串行化传递对象的副本。本身没有被串行化的对象是无法传递给远程方法的。
工作原理
远程对象客户端与服务器端的通信分为多层实现的,如下图的RMI分层模型:
对于程序员来说,客户端就好像在于服务器直接对话, 事实上,客户端程序之与代表远程系统中真实对象的桩对象(stub object)进行对话。桩将会话传递给与传输层对话的远程引用层。客户端的传输层将数据通过Internet传递给服务器的传输层。然后服务器传输层与服务器的远程引用层通信,远程引用层通过与称为骨架(skeleton)的部分对话,这是服务器软件的一部分。骨架与服务器自身对话(Java1.2及以后的版本可以省略骨架层)。另一方向(服务器到客户端)的过程流正好与此相反。从逻辑上看,数据流是水平的(客户端到服务器端,服务器端到客户端),但实际的数据流则是垂直的。
这个过程看起来有些复杂,但我们不需要考虑这些底层细节,就好像打电话,你会考虑电话机是怎么将声音转化为一系列电子脉冲、又如何在另一端将电子脉冲还原为声音的吗?RMI的目标就是,允许程序向方法传递参数及方法的返回值,而不需要考虑这些参数及返回值如何在网络中移动的。
客户端在调用远程对象的方法前,需要该对象的引用,为了要得到该引用,需要基于名请求一个注册表(registry),这个注册表就好像是远程对象的微型DNS,客户端连接此注册表,给出希望得到的远程对象的URL,注册表回复该对象的引用,客户端就可以用它来调用服务器的方法。
事实上,客户端只是在调用桩(stub)中的本地方法。桩是一个本地对象,它实现了远程对象的远程接口;这意味着该桩的方法与远程对象导出的所有方法有着相同签名。如此一来,客户端认为它在调用远程对象的方法,实际是在调用桩中的等价方法。桩存在于客户端的JVM中,以代替存在于服务器的实际对象及方法。当客户端调用一个方法时,桩会将调用传递给远程引用层。
远程引用层执行特定的远程引用协议,此协议与客户端的桩和服务器端的骨架无关,它仅负责理解这个远程引用的意义。有时远程引用可能指向多台主机的多台虚拟机,也有可能执行本地主机中的另一个虚拟机或远程主机中的一个虚拟机。事实上,远程引用层会将桩的本地引用转为服务器上对象的远程引用,不管远程引用的语法与语义是什么,然后将调用传递给传输层。
传输层通过Internet调用。在服务器端,传输层监听入站连接。传输层一接收到调用,就转发给服务器端的远程引用层,远程引用层将客户端发送的引用转为本地虚拟机的引用,然后把请求传递给骨架,骨架读取参数,将数据传递给服务器程序,服务器程序会进行实际的方法调用。当有返回值时,就会在服务器端沿着骨架、远程应用层、传输层向下传,通过Integernet,然后沿着客户端的传输层、远程引用层、桩层向上传。在Java1.2及以后的版本中,会省略骨架层,服务器程序直接与远程引用层对话,其他方法的协议则是相同的。
实现
操作远程对象所需的大部分方法都放在三个包中:java.rmi、java.rmi.server、java.rmi.registry。java.rmi包定义了客户端常见的类、接口和异常,当编写要访问远程对象而本身不是远程对象的程序时,就需要这个包。java.rmi.server包定义了服务器端常见的类、接口和异常,当编写被客户端调用的远程对象时会用到这些类。java.rmi.registry包定义了用于查找和命名远程对象的类、接口和异常。
具体实现:
package rmi.demo;
import java.math.BigInteger;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @Description: 定义远程对象的接口
Remote接口用来标识一个远程对象,它没有任何方法。Remote的子接口确定了远程客户端可以调用的方法。远程对象可能有很多公共方法,但只有在远程接口中声明的方法才能够从远程调用。
*/
public interface Fibonacci extends Remote {
public BigInteger getFibonacci(int i) throws RemoteException;
public BigInteger getFibonacci(BigInteger n) throws RemoteException;
}
package rmi.demo;
import java.math.BigInteger;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* @Description: 定义实现远程接口的类,该实现类应当扩展UnicastRemoteObjet类,该类提供了很多支持远程方法调用的方法,具体地,它能够编组(marshalling)和解组(unmarshalling)对象的引用(编组是把参数和返回值转换为可以通过网络发送的字节流的过程。解组是相反的过程,把字节流转换为一组参数或返回值)。如果扩展UnicastRemoteObjet不方便,可以通过UnicastRemoteObjet的静态方法exportObject()把一个对象导出为远程对象。另还可以通过继承Activatable或Activatable.exportObject()来指定一个远程接口的实现类。Activatable允许客户端跨过服务器关闭和重启的时间,重新连接服务器后,仍能访问相同的远程对象,而UnicastRemoteObjet会在服务器重启后,远程对象就消失了。
该实现类的构造函数调用了UnicastRemoteObject超类的构造函数,它会在某个端口创建一个UnicastRemoteObject,并监听入站连接。
*/
public class FibonacciImpl extends UnicastRemoteObject implements Fibonacci {
private static final long serialVersionUID = 1L;
public FibonacciImpl() throws RemoteException{
super();
}
public BigInteger getFibonacci(int i) throws RemoteException {
return this.getFibonacci(new BigInteger(Long.toString(i)));
}
public BigInteger getFibonacci(BigInteger n) throws RemoteException {
System.out.println("Calculating the " + n + "th Fibonacci number");
BigInteger zero = new BigInteger("0");
BigInteger one = new BigInteger("1");
if(n.equals(zero) || n.equals(one)) {
return one;
}
BigInteger i = one;
BigInteger low = one;
BigInteger high = one;
while(i.compareTo(n) == -1) {
BigInteger tmp = high;
high = high.add(low);
low = tmp;
i = i.add(one);
}
return high;
}
}
package rmi.demo;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
/**
* @Description: 创建一个可以使用远程对象的服务器,需要通过Naming.bind()方法将远程对象添加到注册表中,并为该对象指定一个名字。客户端可以通过该名字请求该对象。
*/
public class FibonacciServer {
public static void main(String[] args) {
try {
FibonacciImpl f = new FibonacciImpl();
Naming.bind("fibonacci", f);
System.out.println("Fibonacci Server ready.");
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
编译桩(stub)
RMI使用桩类,作为客户端和服务器端之间的中间人,服务器上的各个远程对象都以客户端的桩类表示,桩包含了远程接口的信息(即远程接口中定义的方法)。
具体步骤:
- 进入工程的bin目录,我的是“C:\myworkspace\RMIDemo\bin”
- 执行rmic命令,生成桩文件(xxx_stub.class):> rmic -classpath FibonacciImpl
- 启动rmiregistry,>rmiregistry
- 启动服务:>java rmi.demo.FibonacciServer
定义客户端:
package rmi.demo;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @Description: 客户端:要想得到一个远程对象的引用,需要指定一个URL,通过调用Naming.lookup()来查询运行在服务器上的注册表,
URL的格式:rmi://<主机名(或IP):端口号/远程对象在注册表的名字,端口号可选(要根据注册时是否指定来选)
*/
public class FibonacciClient {
public static void main(String[] args) {
if(args.length == 0 || !args[0].startsWith("rmi:")) {
System.err.println("Usage: java FibonacciImpl rmi://host.domain:port/fibonacci number");
return;
}
try {
Object obj = Naming.lookup(args[0]);
Fibonacci calculator = (Fibonacci) obj;
for(int i = 1; i < args.length; i++) {
BigInteger index = new BigInteger(args[i]);
BigInteger f = calculator.getFibonacci(index);
System.out.println("The " + args[i] + "th Fibonacci number is " + f);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
运行: