RMI
1. 简介
Java远程方法调用(Remote Method Invocation, RMI)使得运行在一个Java虚拟机(Java Virtual Machine, JVM)的对象可以调用运行另一个JVM之上的其他对象的方法,从而提供了程序间进行远程通讯的途径。RMI是J2EE的很多分布式技术的基础,比如RMI-IIOP乃至EJB。
我们知道远程过程调用(
Remote Procedure Call, RPC
)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。
Java
的
RMI
则在
RPC
的基础上向前又迈进了一步,即提供分布式
对象
间的通讯,允许我们获得在远程进程中的对象(称为远程对象)的引用(称为远程引用),进而通过引用调用远程对象的方法,就好像该对象是与你的客户端代码同样运行在本地进程中一样。
RMI
使用了术语
”
方法
”
(
Method
)强调了这种进步,即在分布式基础上,充分支持面向对象的特性。
RMI
并不是
Java
中支持远程方法调用的唯一选择。在
RMI
基础上发展而来的
RMI-IIOP
(
Java Remote Method Invocation over the Internet Inter-ORB Protocol
),不但继承了
RMI
的大部分优点,并且可以兼容于
CORBA
。
J2EE
和
EJB
都要求使用
RMI-IIOP
而不是
RMI
。尽管如此,理解
RMI
将大大有助于
RMI-IIOP
的理解。所以,即便你的兴趣在
RMI-IIOP
或者
EJB
,相信本文也会对你很有帮助。另外,如果你现在就对
API
感兴趣,那么可以告诉你,
RMI
使用
java.rmi
包,而
RMI-IIOP
则既使用
java.rmi
也使用扩展的
javax.rmi
包。
分布式对象(
Distributed Object
)
,
指一个对象可以被远程系统所调用。对于
Java
而言,即对象不仅可以被同一虚拟机中的其他客户程序(
Client
)调用,也可以被运行于其他虚拟机中的客户程序调用,甚至可以通过网络被其他远程主机之上的客户程序调用。
分布式对象被调用的过程是这样的:
1.
客户程序调用一个被称为
Stub
(有时译作存根,为了不产生歧义,本文将使用其英文形式)的客户端代理对象。该代理对象负责对客户端隐藏网络通讯的细节。
Stub
知道如何通过网络套接字(
Socket
)发送调用,包括如何将调用参数转换为适当的形式以便传输等。
2.
Stub
通过网络将调用传递到服务器端,也就是分布对象一端的一个被称为
Skeleton
的代理对象。同样,该代理对象负责对分布式对象隐藏网络通讯的细节。
Skeleton
知道如何从网络套接字(
Socket
)中接受调用,包括如何将调用参数从网络传输形式转换为
Java
形式等。
3.
Skeleton
将调用传递给分布式对象。分布式对象执行相应的调用,之后将返回值传递给
Skeleton
,进而传递到
Stub
,最终返回给客户程序。
这个场景基于一个基本的法则,即行为的定义和行为的具体实现相分离。如图所示,客户端代理对象Stub和分布式对象都实现了相同的接口,该接口称为远程接口(Remote Interface)。正是该接口定义了行为,而分布式对象本身则提供具体的实现。对于Java RMI而言,我们用接口(interface)定义行为,用类(class)定义实现。
2.RMI架构
RMI的底层架构由三层构成
:
首先是
Stub/Skeleton(
存根
/
骨架
)
层。该层提供了客户程序和服务程序彼此交互的接口。
然后是远程引用(
Remote Reference
)层。这一层相当于在其之上的
Stub/Skeleton
层和在其之下的传输协议层之前的中间件,负责处理远程对象引用的创建和管理。
最后是传输协议(
Transport Protocol
)
层。该层提供了数据协议,用以通过线路传输客户程序和远程对象间的请求和应答。
RMI具体的调用过程:
当客户程序调用
Stub
时,
Stub
负责将方法的参数转换为序列化(
Serialized
)形式,我们使用一个特殊的术语,即编列(
Marshal
)来指代这个过程。编列的目的是将这些参数转换为可移植的形式,从而可以通过网络传输到远程的服务对象一端。不幸的是,这个过程没有想象中那么简单。这里我们首先要理解一个经典的问题,即方法调用时,参数究竟是传值还是传引用呢?对于
Java RMI
来说,存在四种情况,我们将分别加以说明。
1.
对于基本的原始类型(整型,字符型等等),将被自动的序列化,以传值的方式编列。
2.
对于
Java
的对象,如果该对象是可序列化的(实现了
java.io.Serializable
接口),则通过
Java
序列化机制自动地加以序列化,以传值的方式编列。对象之中包含的原始类型以及所有被该对象引用,且没有声明为
transient
的对象也将自动的序列化。当然,这些被引用的对象也必须是可序列化的。
3.
绝大多数内建的
Java
对象都是可序列化的。
对于不可序列化的
Java
对象(
java.io.File
最典型),或者对象中包含对不可序列化,且没有声明为
transient
的其它对象的引用。则编列过程将向客户程序抛出异常,而宣告失败。
4.
客户程序可以调用远程对象,没有理由禁止调用参数本身也是远程对象(实现了
java.rmi.Remote
接口的类的实例)。此时,
RMI
采用一种模拟的传引用方式(当然不是传统意义的传引用,因为本地对内存的引用到了远程变得毫无意义),而不是将参数直接编列复制到远程。这种情况下,交互的双方发生的戏剧性变化值得我们注意。参数是远程对象,意味着该参数对象可以远程调用。当客户程序指定远程对象作为参数调用服务器端远程对象的方法时,
RMI
的运行时机制将向服务器端的远程对象发送作为参数的远程对象的一个
Stub
对象。这样服务器端的远程对象就可以回调(
Callback
)这个
Stub
对象的方法,进而调用在客户端的远程对象的对应方法。通过这种方法,服务器端的远程对象就可以修改作为参数的客户端远程对象的内部状态,这正是传统意义的传引用所具备的特性。是不是有点晕?
(
那确实
)
这里的关键是要明白,在分布式环境中,所谓服务器和客户端都是相对的。被请求的一方就是服务器,而发出请求的一方就是客户端。
在调用参数的编列过程成功后,客户端的远程引用层从
Stub
那里获得了编列后的参数以及对服务器端远程对象的远程引用(参见
java.rmi.server.RemoteRef API
)。该层负责将客户程序的请求依据底层的
RMI
数据传输协议转换为传输层请求。在
RMI
中,有多种的可能的传输机制,比如点对点(
Point-to-Point
)以及广播(
Multicast
)等。不过,在当前的
JMI
版本中只支持点对点协议,即远程引用层将生成唯一的传输层请求,发往指定的唯一远程对象(参见
java.rmi.server.UnicastRemoteObject API
)。
在服务器端,服务器端的远程引用层接收传输层请求,并将其转换为对远程对象的服务器端代理对象
Skeleton
的调用。
Skeleton
对象负责将请求转换为对实际的远程对象的方法调用。这是通过与编列过程相对的反编列(
Unmarshal
)过程实现的。所有序列化的参数被转换为
Java
形式,其中作为参数的远程对象(实际上发送的是远程引用)被转换为服务器端本地的
Stub
对象。
l
如果方法调用有返回值或者抛出异常,则
Skeleton
负责编列返回值或者异常,通过服务器端的远程引用层,经传输层传递给客户端;相应地,客户端的远程引用层和
Stub
负责反编列并最终将结果返回给客户程序。
l
整个过程中,可能最让人迷惑的是远程引用层。这里只要明白,本地的
Stub
对象是如何产生的,就不难理解远程引用的意义所在了。远程引用中包含了其所指向的远程对象的信息,该远程引用将用于构造作为本地代理对象的
Stub
对象。构造后,
Stub
对象内部将维护该远程引用。真正在网络上传输的实际上就是这个远程引用,而不是
Stub
对象。
3.RMI对象服务
在
RMI
的基本架构之上,
RMI
提供服务与分布式应用程序的一些对象服务,包括对象的命名
/
注册(
Naming/Registry
)服务,远程对象激活(
Activation
)服务以及分布式垃圾收集(
Distributed Garbage Collection, DGC
)。
在前一节中,如果你喜欢刨根问底,可能已经注意到,客户端要调用远程对象,是通过其代理对象
Stub
完成的,那么
Stub
最早是从哪里得来的呢?
RMI
的命名
/
注册服务正是解决这一问题的。当服务器端想向客户端提供基于
RMI
的服务时,它需要将一个或多个远程对象注册到本地的
RMI
注册表中(参见
java.rmi.registry.Registry API
)。每个对象在注册时都被指定一个将来用于客户程序引用该对象的名称。客户程序通过命名服务(参见
java.rmi.Naming API
),指定类似
URL
的对象名称就可以获得指向远程对象的远程引用。在
Naming
中的
lookup()
方法找到远程对象所在的主机后,它将检索该主机上的
RMI
注册表,并请求所需的远程对象。如果注册表发现被请求的远程对象,它将生成一个对该远程对象的远程引用,并将其返回给客户端,客户端则基于远程引用生成相应的
Stub
对象,并将引用传递给调用者。之后,双方就可以按照我们前面讲过的方式进行交互了。
l
注意:
RMI
命名服务提供的
Naming
类并不是你的唯一选择。
RMI
的注册表可以与其他命名服务绑定,比如
JNDI
,这样你就可以通过
JNDI
来访问
RMI
的注册表了。
5. 实战RMI
理论离不开实践,理解
RMI
的最好办法就是通过例子。开发
RMI
的分布式对象的大体过程包括如下几步:
1.
定义远程接口。这一步是通过扩展
java.rmi.Remote
接口,并定义所需的业务方法实现的。
2.
定义远程接口的实现类。即实现上一步所定义的接口,给出业务方法的具体实现逻辑。
3.
编译远程接口和实现类,并通过
RMI
编译器
rmic
基于实现类生成所需的
Stub
和
Skeleton
类。
服务端:
public interface IHello extends Remote {//必须实现Remote接口
public String helloWorld() throws RemoteException;
public String sayHelloToSomeBody(String someBodyName) throws RemoteException;
}
/*
* UnicastRemoteObject 类是一个便捷类,它实现了我们前面所讲的基于 TCP/IP 的点对点通讯机制。
* 远程对象都必须从该类扩展(除非你想自己实现几乎所有 UnicastRemoteObject 的方法)。
* 在我们的实现类的构造函数中,调用了超类的构造函数(当然,即使你不显式的调用这个构建函数,它也一样会被调用。
* 这里这样做,只是为了突出强调这种调用而已)。该构造函数的最重要的意义就是调用 UnicastRemoteObject 类的 exportObject()
* 方法。导出(Export)对象是指使远程对象准备就绪,可以接受进来的调用的过程。
* 而这个过程的最重要内容就是建立服务器套接字,监听特定的端口,等待客户端的调用请求。
*/
public class HelloImpl extends UnicastRemoteObject implements IHello {
public HelloImpl() throws RemoteException {
super();
}
@Override
public String helloWorld() throws RemoteException {
return "hellowWorld";
}
@Override
public String sayHelloToSomeBody(String someBodyName)
throws RemoteException {
return "hello," + someBodyName;
}
public class HelloServer {
public static void main(String[] args) throws Exception,
AlreadyBoundException {
try {
IHello hello = new HelloImpl();
LocateRegistry.createRegistry(8888);
Naming.rebind("rmi://localhost:8888/RHello", hello);
System.out.println(">>>>>INFO:远程IHello对象绑定成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
IHello rhello =(IHello) Naming.lookup("rmi://127.0.0.1:8888/RHello");
System.out.println(rhello.helloWorld());
System.out.println(rhello.sayHelloToSomeBody("sgun"));
}
运行之前,先把server端的IHello接口(对的,只要接口,不需要实现类,这就有点像EJB了)打成jar包放在client的classPath下,先运行服务端如下:>>>>>INFO:远程IHello对象绑定成功!
再运行客户端,结果如下
hellowWorld
hello,sgun
OK,远程调用成功