`

Java远程方法调用(RMI)

    博客分类:
  • java
 
阅读更多

  http://www.blogjava.net/yruc/archive/2007/01/11/93215.html

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 类。

 

package test;

import java.rmi.Remote;
import java.rmi.RemoteException;
//我们的远程接口定义。该接口只有一个方法:executeTask() 用以执行指定的计算任务,并返回相应的结果。
//注意,我们用后缀 Remote 表明接口是远程接口。
public interface ComputerEngineRemote extends Remote {
    public Object excuteTask(Task task) throws RemoteException;

}
*******************************************************************************************************************************************************
package test;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;


/*    UnicastRemoteObject 类是一个便捷类,它实现了我们前面所讲的基于 TCP/IP 的点对点通讯机制。
    远程对象都必须从该类扩展(除非你想自己实现几乎所有 UnicastRemoteObject 的方法)。
    在我们的实现类的构造函数中,调用了超类的构造函数(当然,即使你不显式的调用这个构建函数,它也一样会被调用。
    这里这样做,只是为了突出强调这种调用而已)。该构造函数的最重要的意义就是调用 UnicastRemoteObject 类的
    exportObject() 方法。导出(Export)对象是指使远程对象准备就绪,可以接受进来的调用的过程。
    而这个过程的最重要内容就是建立服务器套接字,监听特定的端口,等待客户端的调用请求。*/

public class ComputerEngine extends UnicastRemoteObject implements
        ComputerEngineRemote {

    protected ComputerEngine() throws RemoteException {
        super();
    }

    public Object excuteTask(Task task) throws RemoteException {
        return task.excute();
    }

}
*******************************************************************************************************************************************************
package test;

import java.io.Serializable;
/*业务接口
execute() 用以执行实际的计算逻辑,并返回结果。
注意,该接口不是远程接口,所以没有扩展 java.rmi.Remote 接口;其方法也不必抛出 java.rmi.RemoteException 异常。
但是,因为它将用作远程方法的参数,所以扩展了 java.io.Serializable 接口。*/
public interface Task extends Serializable {
    Object excute();

}
*******************************************************************************************************************************************************
package test;

public class TaskImpl implements Task {

    public Object excute() {
        return "Hello World!!!";
    }

}
*******************************************************************************************************************************************************
package test;

import java.rmi.Naming;

public class Bootstrap {

/*    为了让客户程序可以找到我们的远程对象,就需要将我们的远程对象注册到 RMI 的注册表。
    这个过程有时被称为"引导"过程(Bootstrap)。我们将为此编写一个独立的引导程序负责创建和注册远程对象。*/
    public static void main(String[] args) throws Exception {
        String name="ComputerEngine";
        
        ComputerEngine engine=new ComputerEngine();
        System.out.println("ComputerEngine exported");
        //将指定名称绑定到一个远程对象
        Naming.rebind(name, engine);
        System.out.println("ComputerEngine bound");
    }

}

/*编译示例程序 
   为远程对象实现创建根和干: 
   要创建存根程序和骨架文件,应以包含远程对象实现的已编译类包全名运行 rmic 编译器。 
   存根(Stub)是远程对象在客户端的代理,它将RMI调用传递给服务器端的骨架(Skeleton),后者负责将该调用传递给实际的远程方法输入如下: 
   E:>rmic -d D:\RMI ComputerEngine (远程接口实现类)执行这个命令, 若rmic成功运行,RMI目录下就会多出两个新类: RmiSampleImpl_Stub.class RmiSampleImpl_Skel.class 它们分别对应的是存根(stub)和骨架(skeleton). 
这里用rmic命令编译生成stud类有一定的技巧,本人就搞了很久才搞出来,老是报找不到类的错误具体解决如下
 *先把所有的*.java文件拷贝到e: est目录下,再把它们的包名全部去掉,再执行下面的过程
E:>rmic -classpath E: est ComputerEngine

运行示例程序

远程对象的引用通常是通过 RMI 的注册表服务以及 java.rmi.Naming 接口获得的。远程对象需要导出(注册)相应的远程引用到
注册表服务,之后注册表服务就可以监听并服务于客户端对远程对象引用的请求。标准的 Sun Java SDK 提供了一个简单的 RMI
注册表服务程序,即 rmiregistry 用于监听特定的端口,等待远程对象的注册,以及客户端对这些远程对象引用的检索请求。 
在运行我们的示例程序之前,首先要启动 RMI 的注册表服务。这个过程很简单,只要直接运行 rmiregistry 命令即可。缺省的
情况下,该服务将监听 1099 端口。如果需要指定其它的监听端口,可以在命令行指定希望监听的端口(如果你指定了其它端口,
需要修改示例程序以适应环境)。如果希望该程序在后台运行,在 Unix 上可以以如下方式运行(当然,可以缺省端口参数): 
$ rmiregistry 1099 &
在 Windows 操作系统中可以这样运行:
E:>start rmiregistry 1099
本人就是直接打的start rmiregistry
我们的 test.Bootstrap 类将用于启动远程对象,并将其绑定在 RMI 注册表中。运行该类后,远程对象也将进入
监听状态,等待来自客户端的方法调用请求。
E: est>java Bootstrap
ComputerEngine exported
ComputerEngine bound
启动远程对象后,打开另一个命令行窗口,运行客户端。命令行的参数为 RMI 注册表的地址,参考下面的示例:

E: est>java rmitutorial
*/
*******************************************************************************************************************************************************
package test;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class Client {

    /**
     * @param args
     * @throws NotBoundException 
     * @throws RemoteException 
     * @throws MalformedURLException 
     */
    public static void main(String[] args) throws Exception {
        String name="rmi://localhost/ComputerEngine";
//         即创建Home接口实例,EJB的原理也就是像这个例子一样,部署到JNDI命名目录上的其实是HOME工厂接口,通过Home工厂
//        可以创建远程接口的实例,调用他的方法.
        ComputerEngineRemote engineRemote=(ComputerEngineRemote) Naming.lookup(name);
        System.out.println(engineRemote.excuteTask(new TaskImpl()));
    }

}

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics