你的位置:首页 > Java教程

[Java教程]java的RMI(Remote Method Invocation)


RMI 相关知识
RMI全称是Remote Method Invocation-远程方法调用,Java RMI在JDK1.1中实现的,其威力就体现在它强大的开发分布式网络应用的能力上,是纯Java的网络分布式应用系统的核心解决方案之一。其实它可以被看作是RPC的Java版本。但是传统RPC并不能很好地应用于分布式对象系统。而Java RMI 则支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。
RMI目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。由于JRMP是专为Java对象制定的,Java RMI具有Java的"Write Once,Run Anywhere"的优点,是分布式应用系统的百分之百纯Java解决方案。用Java RMI开发的应用系统可以部署在任何支持JRE(Java Run Environment Java,运行环境)的平台上。但由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足。不能与用非Java语言书写的对象进行通信。
RMI可利用标准Java本机方法接口JNI与现有的和原有的系统相连接。RMI还可利用标准JDBC包与现有的关系数据库连接。RMI/JNI和RMI/JDBC相结合,可帮助您利用RMI与目前使用非Java语言的现有服务器进行通信,而且在您需要时可扩展Java在这些服务器上的使用。RMI可帮助您在扩展使用时充分利用Java的强大功能。

一、RMI(远程方法调用)的组成
一个正常工作的RMI系统由下面几个部分组成:
·远程服务的接口定义
·远程服务接口的具体实现
·桩(Stub)和框架(Skeleton)文件
·一个运行远程服务的服务器
·一个RMI命名服务,它允许客户端去发现这个远程服务
·类文件的提供者(一个HTTP或者FTP服务器)
·一个需要这个远程服务的客户端程序

二、RMI(远程方法调用)原理示意图


方法调用从客户对象经占位程序(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传 输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。 占位程序扮演着远程服务器对象的代理的角色,使该对象可被客户激活。 远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。服务器端的骨干网完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,占位程序获得返回值。
要完成以上步骤需要有以下几个步骤:
1、 生成一个远程接口
2、 实现远程对象(服务器端程序)
3、 生成占位程序和骨干网(服务器端程序)
4、 编写服务器程序
5、 编写客户程序
6、 注册远程对象
7、 启动远程对象

三、RMI(远程方法调用)的优点
从最基本的角度看,RMI是Java的远程过程调用(RPC)机制。与传统的RPC系统相比,RMI具有若干优点,因为它是Java面向对象方法的一部分。传统的RPC系统采用中性语言,所以是最普通的系统--它们不能提供所有可能的目标平台所具有的功能。
RMI以Java为核心,可与采用本机方法与现有系统相连接。这就是说,RMI可采用自然、直接和功能全面的方式为您提供分布式计算技术,而这种技术可帮助您以不断递增和无缝的方式为整个系统添加Java功能。
RMI的主要优点如下:
面向对象:RMI可将完整的对象作为参数和返回值进行传递,而不仅仅是预定义的数据类型。也就是说,您可以将类似Java哈希表这样的复杂类型作为一个参数进行传递。而在目前的RPC系统中,您只能依靠客户机将此类对象分解成基本数据类型,然后传递这些数据类型,最后在服务器端重新创建哈希表。RMI则不需额外的客户程序代码(将对象分解成基本数据类型),直接跨网传递对象。
可移动属性:RMI可将属性(类实现程序)从客户机移动到服务器,或者从服务器移到客户机。这样就能具备最大的灵活性,因为政策改变时只需要您编写一个新的Java类,并将其在服务器主机上安装一次即可。
设计方式:对象传递功能使您可以在分布式计算中充分利用面向对象技术的强大功能,如二层和三层结构系统。如果您能够传递属性,那么您就可以在您的解决方案中使用面向对象的设计方式。所有面向对象的设计方式无不依靠不同的属性来发挥功能,如果不能传递完整的对象--包括实现和类型--就会失去设计方式上所提供的优点。
安  全:RMI使用Java内置的安全机制保证下载执行程序时用户系统的安全。RMI使用专门为保护系统免遭恶意小应用程序侵害而设计的安全管理程序,可保护您的系统和网络免遭潜在的恶意下载程序的破坏。在情况严重时,服务器可拒绝下载任何执行程序。
便于编写和使用:RMI使得Java远程服务程序和访问这些服务程序的Java客户程序的编写工作变得轻松、简单。远程接口实际上就是Java接口。服务程序大约用三行指令宣布本身是服务程序,其它方面则与任何其它Java对象类似。这种简单方法便于快速编写完整的分布式对象系统的服务程序,并快速地制做软件的原型和早期版本,以便于进行测试和评估。因为RMI程序编写简单,所以维护也简单。
可连接现有/原有的系统:RMI可通过Java的本机方法接口JNI与现有系统进行进行交互。利用RMI和JNI,您就能用Java语言编写客户端程序,还能使用现有的服务器端程序。在使用RMI/JNI与现有服务器连接时,您可以有选择地用Java重新编写服务程序的任何部分,并使新的程序充分发挥Java的功能。类似地,RMI可利用JDBC、在不修改使用数据库的现有非Java源代码的前提下与现有关系数据库进行交互。
编写一次,到处运行:RMI是Java“编写一次,到处运行 ”方法的一部分。任何基于RMI的系统均可100%地移植到任何Java虚拟机上,RMI/JDBC系统也不例外。如果使用RMI/JNI与现有系统进行交互工作,则采用JNI编写的代码可与任何Java虚拟机进行编译、运行。
分布式垃圾收集:RMI采用其分布式垃圾收集功能收集不再被网络中任何客户程序所引用的远程服务对象。与Java 虚拟机内部的垃圾收集类似,分布式垃圾收集功能允许用户根据自己的需要定义服务器对象,并且明确这些对象在不再被客户机引用时会被删除。
并行计算:RMI采用多线程处理方法,可使您的服务器利用这些Java线程更好地并行处理客户端的请求。Java分布式计算解决方案:RMI从JDK 1.1开始就是Java平台的核心部分,因此,它存在于任何一台1.1 Java虚拟机中。所有RMI系统均采用相同的公开协议,所以,所有Java 系统均可直接相互对话,而不必事先对协议进行转换。

四、RMI与CORBA的关系
RMI 和 CORBA 常被视为相互竞争的技术,因为两者都提供对远程分布式对象的透明访问。但这两种技术实际上是相互补充的,一者的长处正好可以弥补另一者的短处。RMI 和 CORBA 的结合产生了 RMI-IIOP,RMI-IIOP 是企业服务器端 Java 开发的基础。1997 年,IBM 和 Sun Microsystems启动了一项旨在促进 Java 作为企业开发技术的发展的合作计划。两家公司特别着力于如何将 Java 用作服务器端语言,生成可以结合进现有体系结构的企业级代码。所需要的就是一种远程传输技术,它兼有 Java 的 RMI(Remote Method Invocation,远程方法调用)较少的资源占用量和更成熟的 CORBA(Common Object Request Broker Architecture,公共对象请求代理体系结构)技术的健壮性。出于这一需要,RMI-IIOP问世了,它帮助将 Java 语言推向了目前服务器端企业开发的主流语言的领先地位。

Java RMI不是什么新技术(在Java1.1的时代都有了),但却是是非常重要的底层技术。
大名鼎鼎的EJB都是建立在rmi基础之上的,现在还有一些开源的远程调用组件,其底层技术也是rmi。

在大力鼓吹Web Service、SOA的时代,是不是每个应用都应该选用笨拙的Web Service组件来实现,通过对比测试后,RMI是最简单的,在一些小的应用中是最合适的。

下面通过一个简单的例子来说明RMI的原理和应用,下面这个例子是一个简单HelloWorld,但已涵盖RMI的核心应用与开发模式。

接口代码

1 public interface IHello extends Remote{2   3   public String helloWorld() throws RemoteException;4   5   public void sayHelloToSomebody(String somebody) throws RemoteException;6 }

实现类:

 1 public class Hello extends UnicastRemoteObject implements IHello/*,Serializable*/ { 2  3   public Hello() throws RemoteException { 4     super(); 5     // TODO Auto-generated constructor stub 6   } 7  8   @Override 9   public String helloWorld() throws RemoteException {10     return "helloWorld";11   }12 13   @Override14   public void sayHelloToSomebody(String somebody) throws RemoteException {15     System.out.println("hello "+somebody+"!");16   }17 18 }

 

服务器端:

 1 public class HelloServer { 2   public static void main(String[] args) { 3      4     try { 5       //创建一个远程对象 6       IHello hello = new Hello(); 7        8       //本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上  9       LocateRegistry.createRegistry(8888);10       //把远程对象注册到RMI注册服务器上,并命名为hello 11       Naming.bind("rmi://localhost:8888/hello", hello);12       13       System.out.println("服务器绑定成功");14       15     } catch (RemoteException e) {16       System.out.println("创建远程对象发生异常!"); 17       // TODO Auto-generated catch block18       e.printStackTrace();19     } catch (MalformedURLException e) {20       System.out.println("发生URL畸形异常!"); 21       // TODO Auto-generated catch block22       e.printStackTrace();23     } catch (AlreadyBoundException e) {24       System.out.println("发生重复绑定对象异常!");25       // TODO Auto-generated catch block26       e.printStackTrace();27     }28   }29 }

客户端:

public class HelloClient {  public static void main(String[] args) {    try {      //在RMI服务注册表中查找名称为hello的对象,并调用其上的方法       IHello hello = (IHello) Naming.lookup("rmi://localhost:8888/hello");            System.out.println(hello.helloWorld());            hello.sayHelloToSomebody("lc");    } catch (MalformedURLException e) {      // TODO Auto-generated catch block      e.printStackTrace();    } catch (RemoteException e) {      // TODO Auto-generated catch block      e.printStackTrace();    } catch (NotBoundException e) {      // TODO Auto-generated catch block      e.printStackTrace();    }  }}

从上面的过程来看,RMI对服务器的IP地址和端口依赖很紧密,但是在开发的时候不知道将来的服务器IP和端口如何,但是客户端程序依赖这个IP和端口。
这也是RMI的局限性之一。这个问题有两种解决途径:一是通过DNS来解决,二是通过封装将IP暴露到程序代码之外。
RMI的局限性之二是RMI是Java语言的远程调用,两端的程序语言必须是Java实现,对于不同语言间的通讯可以考虑用Web Service或者公用对象请求代理体系(CORBA)来实现。

Java RMI 简单示例二
以下用一个文件交换程序来介绍RMI的应用。这个应用允许客户端从服务端交换(或下载)所有类型的文件。第一步是定义一个远程的接口,这个接口指定的签名方法将被服务端提供和被客户端调用。

接口代码:

1 public interface IfileUtil extends Remote{2   public byte[] downloadFile(String filename) throws RemoteException;3 }

实现类代码:

 1 public class FileUtilImpl extends UnicastRemoteObject implements IfileUtil { 2  3   /** 4    *  5   */ 6   private static final long serialVersionUID = 91357143467249225L; 7  8   public FileUtilImpl() throws RemoteException { 9     10   }11   12   13   @Override14   public byte[] downloadFile(String filename) throws RemoteException {15     File file = new File(filename);16 17     byte[] buffer = new byte[(int) file.length()];18 19     int size = buffer.length;20 21     System.out.println("download file size = " + size + "b");22 23     if (size > 1024 * 1024 * 10) {// 限制文件大小不能超过10M,文件太大可能导制内存溢出!24 25       throw new RemoteException("Error:<The File is too big!>");26 27     }28 29     try {30 31       BufferedInputStream input = new BufferedInputStream(32 33       new FileInputStream(filename));34 35       input.read(buffer, 0, buffer.length);36 37       input.close();38 39       System.out.println("Info:<downloadFile() hed execute successful!>");40 41       return buffer;42 43     } catch (Exception e) {44 45       System.out.println("FileUtilImpl: " + e.getMessage());46 47       e.printStackTrace();48       49       return null;50     }51     52   }53 54 }

服务器端代码:

 1 public class FileutilServer { 2   public static void main(String[] args) { 3     try { 4  5       IfileUtil file = new FileUtilImpl(); 6  7       LocateRegistry.createRegistry(8888); 8       // //加上此程序,就可以不要在控制台上开启RMI的注册程序,1099是RMI服务监视的默认端口 9 10       Naming.rebind("rmi://localhost:8888/FileUtilServer", file);11 12       System.out.print("Ready");13 14     } catch (Exception e) {15 16       System.out.println("FileUtilServer: " + e.getMessage());17 18       e.printStackTrace();19 20     }21   }22 }

客户端代码:

 1 public class FileutilClient { 2   public static void main(String[] args) { 3     try { 4      IfileUtil ifileUtil =  (IfileUtil) Naming.lookup("rmi://localhost:8888/FileUtilServer"); 5      byte[] bytes = ifileUtil.downloadFile("D:\\tomcat\\RUNNING.txt"); 6       7      if(bytes == null){ 8        System.out.println("the file is empty"); 9        System.exit(0);10      }11      12      System.out.println(bytes.length);13       14       try {15         File file = new File("copy.java");16         System.out.println("file.getAbsolutePath() = "17             + file.getAbsolutePath());18 19         BufferedOutputStream output = new BufferedOutputStream(20 21         new FileOutputStream(file.getAbsolutePath()));22 23         output.write(bytes, 0, bytes.length);24 25         output.flush();26 27         output.close();28 29         System.out.println("~~~~~End~~~~~");30       } catch (FileNotFoundException e) {31         // TODO Auto-generated catch block32         e.printStackTrace();33       } catch (IOException e) {34         // TODO Auto-generated catch block35         e.printStackTrace();36       }37 38       39     } catch (MalformedURLException e) {40       // TODO Auto-generated catch block41       e.printStackTrace();42     } catch (RemoteException e) {43       // TODO Auto-generated catch block44       e.printStackTrace();45     } catch (NotBoundException e) {46       // TODO Auto-generated catch block47       e.printStackTrace();48     }49   }50 }