Java远程方法调用(Remote Method Invocation, RMI)

RMI
1. 简介
l Java 远程方法调用(Remote Method Invocation, RMI)使得运行在一个Java虚拟机(Java Virtual Machine, JVM)的对象可以调用运行另一个JVM之上的其他对象的方法,从而提供了程序间进行远程通讯的途径。RMI是J2EE的很多分布式技术的基础,比如 RMI-IIOP乃至EJB。
l 我们知道远程过程调用( Remote Procedure Call, RPC )可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。 Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式 对象 间的通讯,允许我们获得在远程进程中的对象(称为远程对象)的引用(称为远程引用),进而通过引用调用远程对象的方法,就好像该对象是与你的客户端代码同样运行在本地进程中一样。 RMI 使用了术语 ” 方法 ” ( Method )强调了这种进步,即在分布式基础上,充分支持面向对象的特性。
l 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 包。
l 分布式对象( Distributed Object ) , 指一个对象可以被远程系统所调用。对于 Java 而言,即对象不仅可以被同一虚拟机中的其他客户程序( Client )调用,也可以被运行于其他虚拟机中的客户程序调用,甚至可以通过网络被其他远程主机之上的客户程序调用。
l 分布式对象被调用的过程是这样的:
1. 客户程序调用一个被称为 Stub (有时译作存根,为了不产生歧义,本文将使用其英文形式)的客户端代理对象。该代理对象负责对客户端隐藏网络通讯的细节。 Stub 知道如何通过网络套接字( Socket )发送调用,包括如何将调用参数转换为适当的形式以便传输等。
2. Stub 通过网络将调用传递到服务器端,也就是分布对象一端的一个被称为 Skeleton 的代理对象。同样,该代理对象负责对分布式对象隐藏网络通讯的细节。 Skeleton 知道如何从网络套接字( Socket )中接受调用,包括如何将调用参数从网络传输形式转换为 Java 形式等。
3. Skeleton 将调用传递给分布式对象。分布式对象执行相应的调用,之后将返回值传递给 Skeleton ,进而传递到 Stub ,最终返回给客户程序。
这个场景基于一个基本的法则,即行为的定义和行为的具体实现相分离。如图所示,客户端代理对象Stub和分布式对象都实现了相同的接口,该接口称为远程接口(Remote Interface)。正是该接口定义了行为,而分布式对象本身则提供具体的实现。对于Java RMI而言,我们用接口(interface)定义行为,用类(class)定义实现。
2 .RMI架构
RMI 的底层架构由三层构成 :
l 首先是 Stub/Skeleton( 存根 / 骨架 ) 层。该层提供了客户程序和服务程序彼此交互的接口。
l 然后是远程引用( Remote Reference )层。这一层相当于在其之上的 Stub/Skeleton 层和在其之下的传输协议层之前的中间件,负责处理远程对象引用的创建和管理。
l 最后是传输协议( Transport Protocol ) 层。该层提供了数据协议,用以通过线路传输客户程序和远程对象间的请求和应答。
RMI 具体的调用过程:
l 当客户程序调用 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 对象的方法,进而调用在客户端的远程对象的对应方法。通过这种方法,服务器端的远程对象就可以修改作为参数的客户端远程对象的内部状态,这正是传统意义的传引用所具备的特性。是不是有点晕? ( 那确实 ) 这里的关键是要明白,在分布式环境中,所谓服务器和客户端都是相对的。被请求的一方就是服务器,而发出请求的一方就是客户端。
l 在调用参数的编列过程成功后,客户端的远程引用层从 Stub 那里获得了编列后的参数以及对服务器端远程对象的远程引用(参见 java.rmi.server.RemoteRef API )。该层负责将客户程序的请求依据底层的 RMI 数据传输协议转换为传输层请求。在 RMI 中,有多种的可能的传输机制,比如点对点( Point-to-Point )以及广播( Multicast )等。不过,在当前的 JMI 版本中只支持点对点协议,即远程引用层将生成唯一的传输层请求,发往指定的唯一远程对象(参见 java.rmi.server.UnicastRemoteObject API )。
l 在服务器端,服务器端的远程引用层接收传输层请求,并将其转换为对远程对象的服务器端代理对象 Skeleton 的调用。 Skeleton 对象负责将请求转换为对实际的远程对象的方法调用。这是通过与编列过程相对的反编列( Unmarshal )过程实现的。所有序列化的参数被转换为 Java 形式,其中作为参数的远程对象(实际上发送的是远程引用)被转换为服务器端本地的 Stub 对象。
l 如果方法调用有返回值或者抛出异常,则 Skeleton 负责编列返回值或者异常,通过服务器端的远程引用层,经传输层传递给客户端;相应地,客户端的远程引用层和 Stub 负责反编列并最终将结果返回给客户程序。
l 整个过程中,可能最让人迷惑的是远程引用层。这里只要明白,本地的 Stub 对象是如何产生的,就不难理解远程引用的意义所在了。远程引用中包含了其所指向的远程对象的信息,该远程引用将用于构造作为本地代理对象的 Stub 对象。构造后, Stub 对象内部将维护该远程引用。真正在网络上传输的实际上就是这个远程引用,而不是 Stub 对象。
3.RMI 对象服务
l 在 RMI 的基本架构之上, RMI 提供服务与分布式应用程序的一些对象服务,包括对象的命名 / 注册( Naming/Registry )服务,远程对象激活( Activation )服务以及分布式垃圾收集( Distributed Garbage Collection, DGC )。
l 在前一节中,如果你喜欢刨根问底,可能已经注意到,客户端要调用远程对象,是通过其代理对象 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 类。

你可能感兴趣的:(java)