JAVA RMI分布式原理和应用

        RMI(Remote Method Invocation)是JAVA早期版本(JDK 1.1)提供的分布式应用解决方案,它作为重要的API被广泛的应用在EJB中。随着互联网应用的发展,分布式处理任务也随之复杂起 来,WebService也得到普遍的推广和应用。
        在某些方面,例如跨语言平台的分布式应用,RMI就显得力不从心了。在实际的应用中,是采用WebService还是传统的RMI来实现?这是一个需要权衡的问题,两者的比较如下所述:

        1. 比起WebService,它只能使用(调用)由JAVA编写的远程服务。而WebService是跨语言平台的,只要客户端和服务端双方都遵守SOAP规范即可;
        2. 
RMI是在TCP协议基础上传递可序列化的java对象(字节数据),而WebService是在HTTP协议基础上通过XML来传输数据的。因此,在同等业务数据量的前提下,RMI的效率要高于WebService
        因此,RMI可以用在业务结构比较简单,而要求实时高效的分布式应用中。 
        从设计角度上讲,JAVA采用的是三层结构模式来实现RMI。在整个体系结构中,有如下几个关键角色构成了通信双方:
        1.客户端:
           1
(StubObject):远程对象在客户端上的代理;
 
          2)远程引用层(RemoteReference Layer):解析并执行远程引用协议;
           3)传输层
(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
        2.服务端:
           1)
骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法,并接收方法执行后的返回值;
 
          2远程引用层(Remote ReferenceLayer):处理远程引用语法之后向骨架发送远程方法调用;
           3)
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
        3.注册表(Registry)
URL形式注册远程对象,并向客户端回复对远程对象的引用。



图1 RMI三层模型 

 

        在实际的应用中,客户端并没有真正的和服务端直接对话来进行远程调用,而是通过本地JVM环境下的桩对象来进行的。
       
1.远程调用过程:
        
1)客户端从远程服务器的注册表中查询并获取远程对象引用。当进行远程调用时,客户端首先会与桩对象(Stub Object)进行对话,而这个桩对象将远程方法所需的参数进行序列化后,传递给它下层的远程引用层(RRL)
        2
桩对象与远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际上是由相应的桩对象代理完成的远程引用层在将桩的本地引用转换为服务器上对象的远程引用后,再将调用传递给传输(Transport)由传输层通过TCP协议发送调用;     
        3在服务器端,传输层监听入站连接,它一旦接收到客户端远程调用后,就将这个引用转发给其上层的远程引用层;
        4
服务器端的远程引用层将客户端发送的远程应用转换为本地虚拟机的引用后,再将请求传递给骨架(Skeleton)
        5)
骨架读取参数,又将请求传递给服务器,最后由服务器进行实际的方法调用。
        2.
结果返回过程:
        1)
如果远程方法调用后有返回值,则服务器将这些结果又沿着“骨架->远程引用层->传输层”向下传递;
        2)
客户端的传输层接收到返回值后,又沿着“传输层->远程引用层->桩”向上传递,然后由桩来反序列化这些返回值,并将最终的结果传递给客户端程序。
        又从技术的角度上讲,有如下几个主要类或接口扮演着上述三层模型中的关键角色:
        1)
注册表:java.rmi.Namingava.rmi.Registry
        2)
骨架:java.rmi.remote.Skeleton       
        3)
桩:java.rmi.server.RemoteStub
        4)远程引用层:
java.rmi.server.RemoteRefsun.rmi.transport.Endpoint
        5)传输层:sun.rmi.transport.
Transport

        作为一般的RMI应用,JAVA为我们隐藏了其中的处理细节,而让开发者有更多的
精力和时间花在实际的应用中。开发RMI的步骤如下所述: 
        1.服务端:
        1)
定义Remote子接口,在其内部定义要发布的远程方法,并且这些方法都要Throws RemoteException
        2)
定义远程对象的实现类,通常有两种方式:
             
a. 继承UnicastRemoteObjectActivatable,并同时实现Remote子接口;
               b. 
只实现Remote子接口和java.io.Serializable接口。
        3)
编译桩(JAVA 1.5及以后版本中,如果远程对象实现类继承了UnicastRemoteObjectActivatable,则无需此步,由JVM自动完成。否则需手工利用rmic工具编译生成此实现类对应的桩类,并放到和实现类相同的编译目录下);
        4)
启动服务器:依次完成注册表的启动和远程对象绑定。另外,如果远程对象实现类在定义时没有继承UnicastRemoteObjectActivatable,则必须在服务器端显示的调用UnicastRemoteObject类中某个重载的exportObject(Remote remote)静态方法,将此实现类对象导出成为一个真正的远程对象。
        2.客户端:
       1)定义用于接收远程对象的
Remote子接口,只需实现java.rmi.Remote接口即可。但要求必须与服务器端对等的Remote子接口保持一致,即有相同的接口名称、包路径和方法列表等。
       2)
通过符合JRMP规范的URL字符串在注册表中获取并强转成Remote子接口对象;
       3)
调用这个Remote子接口对象中的某个方法就是为一次远程方法调用行为。

        下面结合一个例子来说明RMI分布式应用的开发步骤。
        背景:远程系统管理接口SystemManager允许客户端远程调用其内部的某个方法,来获取服务器环境下某个属性的值。因此,第一步需要在服务端和与之通信的客户端环境下,定义一个完全一样的
SystemManager接口,将此接口标记为远程对象。

 

1.在服务端和客户端定义对等Remote子接口(SystemManager)

 

package com.daniele.appdemo.rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

import com.daniele.appdemo.test.domain.User;

/**
 * <p>系统管理远程对象接口</p>
 * @author  <a href="mailto:[email protected]">Daniele</a>
 * @version 1.0.0, 2013-5-21
 * @see    
 * @since   AppDemo1.0.0
 */
public interface SystemManager extends Remote {
   
    /**
     * <p>发布的远程对象方法一:获取所有系统环境信息</p>
     * @author <a href="mailto:[email protected]">Daniele</a>
     * @return
     * @throws RemoteException
     * @since AppDemo1.0.0
     */
    public String getAllSystemMessage() throws RemoteException;
   
    /**
     * <p>发布的远程对象方法二:获取指定的系统环境信息</p>
     * @author <a href="mailto:[email protected]">Daniele</a>
     * @param properties:环境信息对应的属性名,多个名之间用逗号隔开
     * @return
     * @throws RemoteException
     * @since AppDemo1.0.0
     */
    public String getSystemMessage(String properties) throws RemoteException;
}

2.在服务端定义Remote子接口的实现类(SystemManagerImpl),即远程对象的实际行为逻辑。

package com.daniele.appdemo.rmi.server;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;

import org.apache.log4j.Logger;

import com.daniele.appdemo.rmi.SystemManager;
import com.daniele.appdemo.test.domain.User;
import com.daniele.appdemo.util.SystemUtils;

/**
 * <p>
 *         系统管理远程对象实现类。有两种实现方式:
 *      1.继承UnicastRemoteObject或Activatable,并同时实现Remote子接口
 *      2.只实现Remote子接口,这种方式灵活但比较复杂:
 *        1)要求此实现类必须实现java.io.Serializable接口;
 *        2)通过这种方式定义的实现类此时还不能叫做远程对象实现类,
 *          因为在服务器端绑定远程对象之前,还需要利用JDK提供的rmic工具
 *          将此实现类手工编译生成对应的桩实现类,并放到和它相同的编译目录下。
 * </p>
 * @author  <a href="mailto:[email protected]">Daniele</a>
 * @version 1.0.0, 2013-5-21
 * @see    
 * @since   AppDemo1.0.0
 */
public class SystemManagerImpl implements SystemManager, Serializable {
   
//public class SystemManagerImpl extends UnicastRemoteObject implements SystemManager {
   
    private static final long serialVersionUID = 9128780104194876777L;
   
    private static final Logger logger = Logger.getLogger(SystemManagerImpl.class);
   
    private static SystemManagerImpl systemManager = null;

    /**
     * <p>在服务端本地的匿名端口上创建一个用于监听目的的UnicastRemoteObject对象</p>
     * @author <a href="mailto:[email protected]">Daniele</a>
     * @throws RemoteException
     * @since AppDemo1.0.0
     */
    private SystemManagerImpl() throws RemoteException {
        super();
        // 在控制台中显示远程对象被调用,以及返回结果时产生的日志
        RemoteServer.setLog(System.out);
    }
   
    public static SystemManagerImpl getInstance() throws RemoteException {
        if (systemManager == null) {
            synchronized (SystemManagerImpl.class) {
                if (systemManager == null)
                    systemManager = new SystemManagerImpl();
            }
        }
        return systemManager;
    }

    public String getAllSystemMessage() throws RemoteException {
        try {
            /*
             * getClientHost()方法可以获取触发当前远程方法被调用时的客户端的主机名。
             * 在远程服务端的环境中,如果当前线程实际上没有运行客户端希望调用的远程方法时,
             * 则会抛出ServerNotActiveException。
             * 因此,为了尽量避免这个异常的发生,它通常用于远程方法的内部实现逻辑中,
             * 以便当此方法真正的被调用时,可以记录下哪个客户端在什么时间调用了这个方法。
             */
                logger.info("Client {" + RemoteServer.getClientHost() + "} invoke method [getAllSystemMessage()]" );
            } catch (ServerNotActiveException e) {
                e.printStackTrace();
            }
        return SystemUtils.formatSystemProperties();
    }

    public String getSystemMessage(String properties) throws RemoteException {
        try {
            logger.info("Client {"
                    + RemoteServer.getClientHost()
                    + "} invoke method [getAllSystemMessage(String properties)]");
             } catch (ServerNotActiveException e) {
                e.printStackTrace();
            }
        return SystemUtils.formatSystemProperties(properties.split(","));
    }

}

 

 3.由于SystemManagerImpl 不是通过继承UnicastRemoteObject 或 Activatable来实现的,因此在服务器端需要利用JDK提供的rmic工具编译生成对应的桩类。否则,此步略过。例如,在Windows环境下打开命令控制台后  
1)进入工程根目录 
   
         cd d:\ Development\AppDemo
2)
桩编译: 
        
 cmic -classpath WebContent\WEB-INF\classes com.daniele.appdemo.rmi.server.SystemManagerImpl
   语法格式为:
    cmic -classpath <远程对象实现类bin目录的相对路径>
 
<远程对象实现类所在的包路径>.<远程对象实现类的名称>
       
完成后,rmic将在相对于根目录的com\daniele\appdemo\rmi\server子目录中自动生成SystemManagerImpl_Stub桩对象类 (即“远程对象实现类名称_Stub”) 的编译文件,此时需要再将此编译文件拷贝到与远程对象实现类SystemManagerImpl相同的编译目录(\WebContent\WEB-INF\classes\com\daniele\appdemo\rmi\server)中,否则在服务器端发布远程对象时将会抛出java.rmi.StubNotFoundException。如下图所示

 


图2 实现类与桩 

 

   需要特别注意的是:如果服务端中的远程对象实现类存在有对应的桩对象类编译文件,则要求在RMI客户端的环境中,也必须有这个对等的桩对象类编译文件,即意味着这个文件在两端有着相同的包路径、文件名和内部实现细节。因此,最简单的做法就是连同整个包(com\daniele\appdemo\rmi\server)在内,将图2中的SystemManagerImpl_Stub.class文件拷贝到RMI客户端工程的bin目录下即可。如下图。否则,当RMI客户端调用远程服务时将会抛出java.rmi.StubNotFoundException。

 

图3 RMI客户端工程下的对等桩文件
 

 

4.创建用于发布远程对象目的用的服务器(SystemManagerServer)

 

package com.daniele.appdemo.rmi.server;

import java.io.IOException;
import java.util.Arrays; 
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

import org.apache.log4j.Logger;

import com.daniele.appdemo.rmi.SystemManager;

/**
 * <p>
 *         系统管理远程服务器,它主要完成如下任务:
 *      1.在绑定之前先启动注册表服务。
 *         2.将远程对象SystemManager绑定到注册表中,以便让客户端能远程调用这个对象所发布的方法;
 * </p> 
 * @author  <a href="mailto:[email protected]">Daniele</a>
 * @version 1.0.0, 2013-5-21
 * @see     
 * @since   AppDemo1.0.0
 */
public class SystemManagerServer {
    
    private static final Logger logger = Logger.getLogger(SystemManagerServer.class);
    
    public static void main(String[] args) {
        try {
            
                SystemManager systemManager = SystemManagerImpl.getInstance();
            
                /*
                 *  如果远程对象实现类不是通过继承UnicastRemoteObject或Activatable来定义的,
                 *  则必须在服务器端显示的调用UnicastRemoteObject类中某个重载的exportObject(Remote remote)静态方法,
                 *  将此实现类的某个对象导出成为一个真正的远程对象。否则,此步省略。
                 */
                UnicastRemoteObject.exportObject(systemManager);
               
                int port = 9527;
                String url = "rmi://localhost:" + port + "/";
                String remoteObjectName = "systemManager";

                /*
                 *  在服务器的指定端口(默认为1099)上启动RMI注册表。
                 *  必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上。
                 */
                LocateRegistry.createRegistry(port);
            
                /*
                 *  将指定的远程对象绑定到注册表中:
                 *  1.如果端口号不为默认的1099,则绑定时远程对象名称中必须包含Schema,
                 *    即"rmi://<host或ip><:port>/"部分,并且Schema里指定的端口号应与createRegistry()方法参数中的保持一致
                 *  2.如果端口号为RMI默认的1099,则远程对象名称中不用包含Schema部分,直接定义名称即可
                 */
                if (port == 1099)
                    Naming.rebind(remoteObjectName, systemManager);
                else
                    Naming.rebind(url + remoteObjectName, systemManager);
               logger.info("Success bind remote object " + Arrays.toString(Naming.list(url)));
            } catch (IOException e) {
                e.printStackTrace();
            }
      }
    
} 

 

 5.创建发出远程调用请求的客户端(SystemManagerClient)

 

package com.daniele.appdemo.rmi.client;

import java.io.IOException;
import java.rmi.Naming;
import java.rmi.NotBoundException;

import com.daniele.appdemo.rmi.SystemManager;
import com.daniele.appdemo.test.domain.User;

/**
 * <p>系统管理进行远程调用的客户端</p> 
 * @author  <a href="mailto:[email protected]">Daniele</a>
 * @version 1.0.0, 2013-5-21
 * @see     
 * @since   AppDemo1.0.0
 */

public class SystemManagerClient {

    public static void main(String[] args) {
        /*
         * RMI URL格式:Schame/<远程对象名>
         * 1.Schame部分由"rmi://<server host或IP>[:port]"组成,
         *   如果远程对象绑定在服务端的1099端口(默认)上,则port部分可以省略,否则必须指定。
         * 2.URL最后一个"/"后面的值为远程对象绑定在服务端注册表上时定义的远程对象名,
         *   即对应Naming.rebind()方法第一个参数值中最后一个"/"后面的值
         */
        String rmi = "rmi://192.168.1.101:9527/systemManager";
        try {
                /*
                 * 根据URL字符串查询并获取远程服务端注册表中注册的远程对象,
                 * 这里返回的是本地实现了Remote接口的子接口对象(Stub),
                 * 它与服务端中的远程对象具有相同的接口和方法列表,因而作为在客户端中远程对象的一个代理。
                 */
                SystemManager systemManager = (SystemManager) Naming.lookup(rmi);
         
//              System.out.println(systemManager.getAllSystemMessage());
                System.out.println(systemManager.getSystemMessage("java.version,os.name"));
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NotBoundException e) {
                e.printStackTrace();
            }
    }

}

 完成后,再依次到服务器和客户端上启动SystemManagerServer和SystemManagerClient即可。

 

 

猜你喜欢

转载自code727.iteye.com/blog/1874271
今日推荐