RMI--原理及实现浅析

转载自:http://blog.csdn.net/qb2049_xg/article/details/3278672

转载自:http://www.blogjava.net/yeshucheng/archive/2009/02/02/252932.html

官方文档:http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/index.html


综述

       Rmi自从JDK1.1就已经出现了。而对于为什么在JAVA的世界里需要一个这样 思想理念就需要看下:RMI问世由来。其实真正在国内使用到它的比较少,不过在前些年比较火的EJB就是在它的基础上进一步深化的。从本质上来讲RMI的兴起正是为了设计分布式的客户、服务器结构需求而应运而生的,而它的这种B/S结构思想能否和我们通常的JAVA编程更加贴切呢?言外之意就是能否让这种分布式的状态做到更加透明,作为开发人员只需要按照往常一样开发JAVA应用程序一样来开发分布式的结构。那现在的问题是如何来划平这个鸿沟呢?首先我们来分析下在JAVA世界里它的一些特点因素:

l         JAVA使用垃圾收集确定对象的生命周期。

l         JAVA使用异常处理来报告运行期间的错误。这里就要和我们网络通讯中的异常相联系起来了。在B/S结构的网络体系中我们的这种错误性是非常常见的。

l         JAVA编写的对象通过调用方法来调用。由于网络通讯把我们的客户与服务器之间阻隔开了。但是代理的一种方式可以很好的提供一种这样的假象,让开发人员或者使用者都感觉是在本地调用。

l         JAVA允许一种高级的使用类加载器(CLassLoader)机制提供系统类路径中没有的类。这话什么意思?


主要特点

上面说到了分布式的方式和我们的JAVA中如何更好的划平这个鸿沟,需要具备的特质。

那这里我们来看看我们所谓的RMI到底跟我们普通的JAVA(或者说JavaBean)存在一些什么样的差异:

l         RMI远程异常(Remote Exception):在上面我们也提到了一个网络通讯难免有一些无论是软件级别的还是硬件级别的异常现象,有时候这些异常或许是一种无法预知的结果。让我们开发人缘如何来回溯这种异常信息,这个是我们开发人员要关心的。因此在调用远程对象的方法中我们必须在远程接口中(接口是一种规范的标准行为)所以在调用的这个方法体上需要签名注明:java.rmi,RemoteException.。这也就注明了此方法是需要调用远程对象的。

l         值传递 :当把对象作为参数传递给一个普通的JAVA对象方法调用时,只是传递该对象的引用。请注意这里谈到的是对象的“引用”一词,如果在修改该参数的时候,是直接修改原始对象。它并不是所谓的一个对象的备份或者说拷贝(说白了就是在本JVM内存中的对象)。但是如果说使用的是RMI对象,则完全是拷贝的。这与普通对象有着鲜明的对比。也正是由于这种拷贝的资源消耗造就了下面要说到的性能缺失了。

l         调用开销:凡是经过网络通讯理论上来说都是一种资源的消耗。它需要通过编组与反编组方式不断解析类对象。而且RMI本身也是一种需要返回值的一个过程定义。

l         安全性:一谈到网络通讯势必会说到如何保证安全的进行。

 

概念定义

在开始进行原理梳理之前我们需要定义清楚几个名词。对于这些名词的理解影响到后的深入进行。

1.         Stub(存根,有些书上也翻译成:桩基在EJB的相关书籍中尤为体现这个意思):

这里举例说明这个概念起(或许不够恰当)。例如大家因公出差后,都有存在一些报销的发票或者说小票。对于你当前手头所拿到的发票并不是一个唯一的,它同时还在你发生消费的地点有一个复印件,而这个复印件就是所谓的存根。但是这个存根上并没有很多明细的描述,只是有一个大概的金额定义。它把很多的细节费用都忽略了。所以这个也是我们说的存根定义。而在我们RMI的存根定义就是使用了这样一个理解:在与远程发生通讯调用时,把通讯调用的所有细节都通过对象的封装形式给隐藏在后端。这本身就符合OOAD的意思理念。而暴露出来的就是我们的接口方式,而这种接口方式又和服务器的对象具有相同的接口(这里就和我们前面举例说的报销单据联系上了,报销单据的存根不知道会有一个什么形式发生具体问题,而你手执的发票具体就需要到贵公司去报销费用,而这里的公司财务处就是所谓的服务器端,它才是真正干实质性问题的。)因此作为开发人员只需要把精力集中在业务问题的解决上,而不需要考虑复杂的分布式计算。所有这些问题都交给RMI去一一处理。

2.         Skeleton(一些书翻译叫骨架,也叫结构体):它的内部就是真正封装了一个类的形成调用体现机制。包括我们熟知的ServerSocket创建、接受、监听、处理等。

3.         Mashalling(编组):在内存中的对象转换成字节流,以便能够通过网络连接传输。

4.         Unmashalling(反编组):在内存中把字节流转换成对象,以便本地化调用。

5.         Serialization(序列化):编组中使用到的技术叫序列化。

6.         Deserializationg(反序列化):反编组中使用到的技术叫反序列化。

 

客户端

       既然我们知道stub主要是以接口的方式来暴露体现,而stub主要 也是以代理的方式来具体实施。那在RMI中的这种接口有哪些特性呢?(Remote Interface

1)        必须扩展(extendsjava.rmi.Remote接口,因为远程接口并不包含任何一个方法,而是作为一个标记出现,它就是需要告诉JVMRunTime的时候哪些是常规对象,哪些属于远程对象。通过这种标识的定义能让JVM了解类中哪些方法需要编组,通过了编组的方式才能通过网络序列化的调用;

2)        接口必须为public(公共),它的好处不言而喻的——能够方便的让所有人员来调用。

3)        接口方法还需要以异常抛出(例如:RemoteException),至于它的用处我们在前面也提到这里就不再复述;

4)        在调用一个远程对象期间(运行期间),方法的参数和返回值都要必须是可序列化的。至于为什么需要这么做?这里的缘由不用多说大家也应该清楚了解。


服务端

       既然我们知道stub所做的事情是一个简单的代理转发动作,那我们真正要做的对象就在服务端来做了。对于使用简单的RMI我们直接去指定,但是往往一旦使用了RMI对象就存在非常多的远程方法调用,这个时候服务器端对于这么多的调用如何来判别或者说识别呢?这里就要说到的是对于RMI实现它会创建一个标识符,以便以后的stub可以调用转发给服务器对象使用了,而这种方式我们通常叫服务器RMI的注册机制。言外之意就是让服务器端的对象注册在RMI机制中,然后可以导出让今后的stub按需来调用。那它又是如何做到这种方式的呢?对于RMI来说有两种方式可以达到这种效果:

a)         直接使用UnicastRemoteObject的静态方法:exportObject

b)        继承UnicastRemoteObject类则缺省的构造函数exportObject

现在大家又会问他们之间又有什么区别呢?我该使用哪种方式来做呢,这不是很难做抉择吗?从一般应用场景来说区别并不是很大,但是,这里说了“但是”哦,呵呵。大家知道继承的方式是把父类所具备的所有特质都可以完好无损的继承到子类中而对于类的总老大:Object来说里面有:equals()hashCode()toString()等方法。这是个什么概念呢?意思就是说如果对于本地化的调用,他们两个的方法(a,b)基本区别不是很大。但是我们这里强调的RMI如果是一种分布式的特定场景,具备使用哈希表这种特性就显得尤为重要了。

刚才说了服务端采用什么方法行为导出对象的。那现在导出后的对象又对应会发生什么情况呢?

首先被导出的对象被分配一个标识符,这个标识符被保存为:java.rmi.server.ObjID对象中并被放到一个对象列表中或者一个映射中。而这里的ID是一个关键字,而远程对象则是它的一个值(说到这大家有没有觉得它原理非常像HashMap的特质呢?没错,其实就是使用了它的特性),这样它就可以很好的和前面创建的stub沟通。如果调用了静态方法UnicastRemoteObject.export(Remote …)RMI就会选择任意一个端口号,但这只是第一调用发生在随后的exportObject每次调用都把远程对象导出到该远程对象第一被导出时使用的端口号。这样就不会产生混乱,会把先前一一导出的对象全部放入到列表中。当然如果采用的是指定端口的,则按照对应显示的调用方式使用。这里稍作强调的是一个端口可以导出任意数目的对象。


简单例子

   
   接口类:
package org.shirdrn.rmi.server;

import java.io.IOException;

public interface ShirdrnService {
    public String getServerTime() throws IOException, ClassNotFoundException;
    public String getGreetings () throws IOException, ClassNotFoundException;
}
   接口实现类:
package org.shirdrn.rmi.server;

import java.io.IOException;

import java.util.Date;

public class ShirdrnServiceImpl implements ShirdrnService {
    public ShirdrnServiceImpl() {
    }

    public String getServerTime() throws IOException, ClassNotFoundException{
        Date date = new Date();
        String time = date.toLocaleString();
        return time;
    }

    public String getGreetings() throws IOException, ClassNotFoundException{
        Date date = new Date();
        int hour = date.getHours();
        String greetings = "";
        if(hour < 12 && hour > 6){
            greetings = "上午好!";
        }
        else if(hour <= 18 && hour > 12){
                greetings = "下午好!";
            }
            else{
                greetings = "晚上好!";
            }
        return greetings;
    }
}
   客户端stub类:
package org.shirdrn.rmi.client;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import org.shirdrn.rmi.server.ShirdrnService;

public class ShirdrnStub implements ShirdrnService{

    Socket socket;

    public ShirdrnStub() {
        try {
            socket = new Socket("56987b31c0b246d",8888);
        } catch (UnknownHostException e) {
            e.printStackTrace();
            System.out.println("[信息]创建套接字异常:未知的主机名称。");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getServerTime() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        oos.writeObject("getServerTime");
        oos.flush();
        ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); 
        return (String)ois.readObject();
    }

    public String getGreetings() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        oos.writeObject("getGreetings");
        oos.flush();
        ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); 
        return (String)ois.readObject();
    }
}
   服务端Skeleton类:
package org.shirdrn.rmi.server;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ShirdrnSkeleton extends Thread {
    ShirdrnServiceImpl shirdrnServiceImpl;
    public ShirdrnSkeleton(ShirdrnServiceImpl shirdrnServiceImpl) {
        this.shirdrnServiceImpl = shirdrnServiceImpl;
    }
    public void run() {          
        try {
            ServerSocket serverSocket = new ServerSocket(8888);        
            Socket socket = serverSocket.accept();
            while(socket != null){
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                String method = (String)ois.readObject();
                if(method.equals("getServerTime")){
                    String serverTime = shirdrnServiceImpl.getServerTime();
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(serverTime);
                    oos.flush();
                }
                if(method.equals("getGreetings")){
                    String greetings = shirdrnServiceImpl.getGreetings();
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(greetings);
                    oos.flush();
                }
            }
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
    
    public static void main(String[] args){
        ShirdrnServiceImpl ssi = new ShirdrnServiceImpl();
        ShirdrnSkeleton skeleton = new ShirdrnSkeleton(ssi);
        skeleton.start();
    }   
}
   客户端调用
package org.shirdrn.rmi.client;

import java.io.IOException;
import org.shirdrn.rmi.server.ShirdrnService;

public class ShirdrnClient {
    public ShirdrnClient() {
    }

    public static void main(String[] args) throws IOException,ClassNotFoundException {
        ShirdrnService stub = new ShirdrnStub();
        System.out.println("正在获取服务器时间...");
        System.out.println("服务器时间 : "+stub.getServerTime());
        System.out.println("正在获取问候语...");
        System.out.println("问候语为 : "+stub.getGreetings());        
    }
}

博采众生现代的 网络和编程技术,让我们的资源共享从信息逐步迈向了硬件,你想到过吗?你可以把一个解密的任务放到远程的运行更快的,闲置的服务器上进行,最后把运算的结果返回来,你所要做的就是提交任务。看到了吧,处理这些任务是远程的服务器。

Java的RMI的技术让这种实现变得非常的容易,本文就从基础性的东西讲解一下RMI的原理,有关安全和部署的问题,本文不加于讨论。 读过本文希望对你的学习有所帮助。

本文将讨论以下内容:

  • 一、RMI的定义
    • 1、RMI定义和功能
    • 2、Stub和Skeleton介绍
  • 二、RMI原理的浅析
    • 1、RMI的应用开发流程
    • 2、Stub和Skeleton在什么位置产生
    • 3、Stub和公共接口(远程对象的功能)的关系
    • 4、rmiregistry介绍
  • 三、RMI简单实现

一、RMI的定义

1、RMI定义和功能

RMI是Remote Method Invocation的简称,是J2SE的一部分,能够让程序员开发出基于Java的分布式应用。一个RMI对象是一个远 程Java对象,可以从另一个Java虚拟机上(甚至跨过网络)调用它的方法,可以像调用本地Java对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

2、Stub和Skeleton介绍

在学习RMI的时,我们不能不讨论stub和skeleton作用和相关问题。他们是我们理解RMI原理的关键。我做个比方说明这两个概念。 假如你是A,你想借D的工具,但是又不认识D的管家C,所以你找来B来帮你,B认识C。B在这时就是一个代理,代理你的请求,依靠自己的话语去借。C呢他负责D家东西收回和借出 ,但是要有D的批准。在得到D的批准以后,C再把东西给B,B呢再转给A。stub和skeleton在RMI中就是角色就是B和C,他们都是代理角色,在现实开发中隐藏系统和网络的的差异, 这一部分的功能在RMI开发中对程序员是透明的。Stub为客户端编码远程命令并把他们发送到服务器。而Skeleton则是把远程命令解码,调用服务端的远程对象的方法,把结果在编 码发给stub,然后stub再解码返回调用结果给客户端。所有的操作如下图所示:

RMI--原理及实现浅析_第1张图片

二、RMI原理的浅析

1、RMI的应用开发流程

为了方便理解,在这里我先介绍RMI应用开发的流程。如果你在网上搜索RMI,你会看到好多有关RMI的经典例子。在这里需要说明的就是 从JDK5.0以后,stubs和skeleton的类的产生不需要使用单独的rmic编译器了,所以本文开发流程就简化为客户端和服务端,没有中间的stub和skeleton的环节。整个开发流程如下 图:

RMI--原理及实现浅析_第2张图片

公共接口是客户端和服务端共同存在的,定义了远程对象将要实现的功能,客户端依这个接口调用的方法,在服务端都必须实现。随后 我们要开发实现接口的,继承java.rmi.UnicastRemoteObject的实现类,暂且把其称为Impl,这个类我们将在随后的服务端类中实例化并把其注册到RMI注册表中。运行RMI注册表 服务,具体各个平台的启动方法,请你参考JDK文档。而余下的问题就是开发服务端类和客户端类了,在服务类中,需要实例化Impl,并用一个字符串把这个对象绑定到RMI注册表 中;客户端需要实现检索RMI注册表获得需要远程对象的stub,进行远程方法调用。关于如何的实现,请网友看本文的RMI简单实现部分。

2、Stub和Skeleton在什么位置产生

网上有太多的stub和skeleton的疑问了,对stub和skeleton迷惑影响了对RMI的学习。从JDK5.0以后,这两个类就不需要rmic来产生了 ,而是有JVM自动处理,实际上他们还是存在的。Stub存在于客户端,作为客户端的代理,让我们总是认为客户端产生了stub,接口没有作用。实际上stub类是通过Java动态类下载 机制下载的(具体内容请参考:Java RMI实现代码动态下载),它是由服 务端产生,然后根据需要动态的加载到客户端,如果下次再运行这个客户端该存根类存在于classpath中,它就不需要再下载了,而是直接加载。(具体的内部细节,需要参考Sun 的Rmi - Java Remote Method Invocation – Specification)。总的来说,stub是在服务端产生的,如果服务端的stub内容改变,那么客户端的也是需要同步更新。

3、Stub和公共接口interface(远程对象的功能)的关系

我的一篇译文上,一个网友说 “如果客户端有接口的话,我还要那个存根来干什么??直接用接口不就完事了?经过测试,客户端放一个接口的话,根本不需要存根,这个是我一直想不明白的..........”。在开始 学习时,我也是有这个疑问,通过接口直接调用不就OK了吗?实际上,存根是我们必需的。一个服务如果没有合适的存根类,客户就没有办法去调用远程的接口,RMI使用存根来返 回引用远程对象接口的参数。在类的关系中,接口是不能实例化的,但是它可以指向一个实现该接口的实例。存根和接口就是这种关系,而存根类的实例就是在:lookup ()方法 调用时加载、实例的。接口只是告诉JVM,内存中这片的字节码中,这几个方法我可以调用。

4、rmiregistry介绍

我们用一个图示来说明rmiregistry的作用:

RMI--原理及实现浅析_第3张图片

Rmiregistry需要在提供远程对象服务端启动,它提供了一个环境,说白了就是在内存中,开辟了一片空间,用来接受服务端服务程序的 注册,产生一个类似于数据库,提供存储检索远程对象功能的注册表。这个RMI注册表,就是我们常听到的RMI名字服务。远程客户端,就是依靠它获得存根,调用远程方法。说到 这里有一个端口的问题,如果你在启动rmiregistry时,设定了非默认端口,那么需要在服务端和客户端统一使用该端口,否则就会有RemoteException的异常抛出,rmiregistry提 供的服务是针对特定的端口号的,不然在同一台机器上也是无法提供服务。

到此,有关RMI的原理的东西,就简单地介绍了,如果你还有什么疑问,请你留言,接下来我们来进行一个实例,复杂一些的实例(含带 UI),我在下一篇文章中介绍。

三、RMI简单实现

首先,我的环境:Windows XP,JDK1.6。所以我在这个实例中不再使用rmic编译stub和skeleton了。要实现的就是两个部分:客户端和 服务端。我们使用经典的Hello,world!例子。

文件清单如下:

  • 接 口:MyRmiInterface.java
  • 客户端:MyRmiClient.java
  • 服务端:MyRmiImpl.java(实现类) MyRmiServer.java

    你可以采用两台电脑,也可以一台,但是MyRmiInterface.class和MyRmiClient.class存在于客户端,MyRmiInterface.class, MyRmiImpl.class与MyRmiServer.class存在服务端。所有程序代码清单如下:

    客户端:

    1. package net.csdn.blog.qb2049.exam;
    2. //MyRmiInterface.java
    3. import java.rmi.*;
    4. //这个接口必需继承Remote 接口
    5. public interface MyRmiInterface extends Remote
    6. {
    7.  //声明方法 时,必需显式地抛出RemoteException异常
    8.  public String sayHello() throws RemoteException;
    9. }
    *************************************************************
    1. package net.csdn.blog.qb2049.exam;
    2. import java.rmi.*;
    3. import java.rmi.registry.*;
    4. //MyRmiClient.java
    5. public class MyRmiClient
    6. {
    7.  public MyRmiClient(){}
    8.  public static void main(String args[])
    9. {
    10. String host=(args.length<1)?null:args [0];
    11.  try{
    12.  //获得运行rmiregistry服务的主机上的注 册表
    13. Registry registry=LocateRegistry.getRegistry(host);
    14.  //查询并获得远程对象的存根
    15. MyRmiInterface stub=(MyRmiInterface) registry.lookup("Hello");
    16.  //像在使用本地对象方法那样,调用远程方法
    17. String response=stub.sayHello();
    18. System.out.println("response:"+response);
    19. }catch(Exception e)
    20. {
    21. System.out.println("Client exception :"+ e.toString());
    22. e.printStackTrace();
    23. }
    24. }
    25. }

    服务端:(服务端也是需要存在接口)

    1. package net.csdn.blog.qb2049.exam;
    2. import java.rmi.*;
    3. import java.rmi.server.*;
    4. //这个类继承 UnicastRemoteObject非常重要
    5. //MyRmiImpl.java
    6. public class MyRmiImpl extends UnicastRemoteObject implements MyRmiInterface
    7. {
    8.  //在实例化这个类时,就导出了远程对象,该构造方法必需
    9.  public MyRmiImpl() throws RemoteException
    10. {
    11.  super();
    12. }
    13.  //实现接口中的方法,这个时间不需要显 式抛出RemoteException异常了
    14.  public String sayHello()
    15. {
    16.  return "Hello ,world";
    17. }
    18. }
    *************************************************************
    1. package net.csdn.blog.qb2049.exam;
    2. import java.rmi.*;
    3. import java.rmi.registry.*;
    4. import java.rmi.server.*;
    5. //MyRmiServer.java
    6. public class MyRmiServer
    7. {
    8.  public static void main(String args[])
    9. {
    10.  try{
    11.  //实例化远程对象,同时导出了该对象
    12. MyRmiImpl server=new MyRmiImpl ();
    13.  //获得本地RMI注册表对象
    14. Registry registry=LocateRegistry.getRegistry();
    15.  //在注册表中绑定远程对象
    16. registry.bind("Hello",server);
    17.  //通告服务端已准备好了
    18. System.out.println("System already!");
    19. }catch(RemoteException e)
    20. {
    21. System.out.println("在建立远程连接的情况出现了异 常"+e.getMessage());
    22. System.out.println(e.toString());
    23. catch(AlreadyBoundException e1){
    24. System.out.println("在向注册表 中绑定时出现了问题,名字已被绑定了!
    25. /n"+e1.getMessage());
    26. }
    27. }
    28. }
    *************************************************************

    实验环境目录如下图示:

    RMI--原理及实现浅析_第4张图片

    编译服务端

    RMI--原理及实现浅析_第5张图片

    编译客户端

    RMI--原理及实现浅析_第6张图片

    启动注册表环境,它没有任何输出

    RMI--原理及实现浅析_第7张图片

    启动服务端,提示ready!

    RMI--原理及实现浅析_第8张图片

    启动客户端端,得到Hello World!

    RMI--原理及实现浅析_第9张图片

    编译中可能出现的问题

    启动中,可能由于codebase设置出现问题,总是抛出ClassNotFoundException异常,使你的服务器程序无法启动,建议你参考Java RMI实现代码动态下载来寻求帮助!

    本文参考:

  • 1、Java与CORBA 客户/服务器编程(第二版) 亢勇等译 电子工业出版社
  • 2、Sun公司JDK文档
  • 3、Understanding Java RMI Internals  Ahamed Aslam.K  Develper.com
  • 4、Java2 核心技术卷II 王浩、姚建平等译 机械工业出版社
  • 5、Java远程方法调用(Remote Method Invocation, RMI)  http://www.blogjava.net/yruc/
  • 你可能感兴趣的:(java,rmi)