我的 RMI 学习笔记(1)

java 44 2016-02-29 13:03

前言

背景介绍

RMI是Java的一组拥护开发分布式应用程序的API。RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(Java Remote Method Protocol)。

Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议

通过RMI技术,本地虚拟机JVM可以调用存在于另外一个JVM中的对象方法,就好像该虚拟机调用存在于本地JVM的某个对象方法一样。而另外一个JVM可以与本地JVM在同一台物理机,也可以属于不同的物理机。

常见的可以实现远程调用的技术还包括了 RPC,CORBA,Web Service,这里不一一介绍,需要了解的同学可以 google,以下只关注 RMI 这个只针对 Java 语言的远程调用技术的一些阐述。

RMI技术变迁

RMI技术早在JDK1.1中就出现了,并且在JDK1.5版本做了重大改进,使得我们可以更加简单的使用RMI技术来支持分布式应用。关于详细的技术变更历史,可以参见官方的 Java RMI release Note

RMI基本实现说明

我们希望在两个JVM虚拟机远程方法调用可以以常规方法进行调用,而无需关心数据的发送接收以及解析之类的问题。解决的思路是,在客户端安装一个代理(proxy),代理是位于客户端虚拟机中的一个对象,他对于客户端对象,看起来就像访问的远程对象一样,客户端代理会使用网络协议与服务器进行通信。同样的实现的服务端代码的程序员也不希望需要手动编码对客户端的调用进行复杂的处理,所以在服务器端也会有一个代理对象来进行通信的繁琐工作。

在RMI中,客户端的代理对象被称为存根(Stub),存根位于客户端机器上,它知道如何通过网络与服务器联系。存根会将远程方法所需的参数打包成一组字节。对参数编码的过程被称为参数编组(parameter marshalling),参数编组的目的是将参数转换成适合在虚拟机之间进行传递的形式。在RMI协议中,对象时使用序列化机制进行编码的。

总的来说,客户端的存根方法构造了一个信息块,它由以下几部分组成

  • 被使用的远程对象的标识符

  • 被调用的方法的描述

  • 编组后的参数

然后,存根将此信息发送给服务器。在服务器的一端,接收器对象执行以下动作:

  1. 定位要调用的远程对象

  2. 调用所需的方法,并传递客户端提供的参数

  3. 捕获返回值或调用产生的异常。

  4. 将返回值编组,打包送回给客户端存根

客户端存根对来自服务器端的返回值或异常进行反编组,其结果就成为了调用存根返回值。

几个重要概念说明

RMI 注册表(参见 LocateRegistry 类)

  1. 为了使得客户端能够查找到服务端对外提供的远程对象,RMI需要维护一个RMI注册表,该注册表维护了对于客户端而言的远程对象位置,对外提供了服务,服务端需要将要外提供服务的对象的代理绑定到RMI注册表中。

  2. RMI注册表可以跟服务端不在一台主机上。

  3. RMI注册表的启动有两种方式:一种是通过命令行 rmiregistry $port 在命令行启动;另外一种是通过 LocateRegistry 类的 createRegistry(int port) 方法启动。

客户端查找远程对象,服务端注册远程对象的多样性

也许在网上查看各种RMI示例代码时我们常常发现客户端查找远程对象,服务端注册远程对象使用的代码都不尽相同,我在学习过程中见到的有三种途径:

  1. LocateRegistry 类的对象的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的

  2. 利用命名服务 java.rmi.Naming 类的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的

  3. 利用JNDI(Java Naming and Directory Interface,Java命名和目录接口) java.naming.InitialContext 类来 rebind() 和 lookup() 来实现绑定注册和查找远程对象的

实际上 java.rmi.Naming 类的 rebind() 和 lookup()方法只是在 LocateRegistry 类的包了一层皮,大家可以查看该类的源码。

RMI涉及到安全机制方面的问题

看到这篇文章的同学也许已经在本机上地运行了网上某些示例代码,然而在本机环境中的成功运行,实际上对于真正的分布式应用来说是缺乏真实的参照性的,我在完成本机运行后实际上对于RMI的认识还是非常肤浅的,而网络上对于在服务端和客户端程序运行的两台不同物理主机的RMI示例内容很少,而Oracel提供的官方文档 JDK 1.5的RMI教程 最新的教程 也没有一个可以让我完美地在两台机器上跑起来。
对于在本机运行客户端、服务端以及RMI注册表服务不涉及到安全性问题,当我们把客户端的代码扔到另一台物理机,首先遇到的问题是 no security manager: RMI class loader disable; 这个问题,这时,我们需要做的首先是了解 Java 的安全机制中涉及到 SecurityManager 类和安全策略文件 policy 的写法和各个字段的含义。

SecurityManager 的加入方式:

  1. 在编码中加入

    System.setSecurityManager(new SecurityManager());
    
  2. 在命令行中加入
    java -Djava.security.manager

既然加入了安全管理器,那么也需要对应的安全策略文件,如果主动指定策略文件,将使用默认策略文件:

  1. 在编码中加入

    System.setProperty("java.security.policy", "策略文件路径")

  2. 在命令行加入

    java -Djava.security.policy=MyApp.policy

关于 policy 文件的形式说明:

    grant codesource{
        permission1;
        permission2;
        ...
    };
    
    codesource 的一般形式是 codeBase "url地址"
    基本含义是赋予 codesource 中的代码 permission1, permission2 ... 等权限
    如果省略的 codesource 如下面这样,说明 permission 权限被赋予给了所有代码
    grant{
        permission1;
    } 

远程类文件的加载

在两台不同的物理主机部署RMI程序时碰到的另外常见问题是 ClassNotFoundException, 这个问题产生的原因对于客户端来说有3个:

  1. 本机运行的客户端的客户端代码 -classpath 的指定问题。

  2. 本机运行的客户端代码中需要跟服务端代码同步的部分不一致,RMI运行过程中会检查一致性,比如 Remote 接口的代码与服务端不一致,我在部署过程中服务端的远程接口是位于包中,而我在客户端的远程接口代码我为了图方便自己写的,结果没有在包中,最后在解决了 securityManager 问题后一直卡在这里,最后发现了包名出现在异常中,索性将服务端代码都放到默认包中了,问题立马解决了。

  3. RMI运行时客户端有时需要从远端获取.class文件(由于本地 classpath 中没有,而远程方法中的参数或则返回值包含了可序列化的对象,此时RMI可以通过网络从指定路径获取),参见使用RMI进行动态代码下载
    使用 -Djava.rmi.server.codebase= http://webvector/export/(示例地址) 来指定

实际上客户端,服务端,RMI注册表,公共类文件可以分别位于不同主机上,此时涉及到的 ClassNotFoundException 则会更加多,这里不在赘述,出错的原因都是类似的,客户端,服务端,RMI注册程序运行时都可以指定 -Djava.rmi.server.codebase=http://webvector/export/(示例地址) 参数

开始动手

为了避免包结构带来的干扰,下面代码不在包结构中

在同一台主机上部署RMI程序

定义一个远程对象的接口

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

public interface Hello extends Remote{ // 远程对象的接口必须拓展 java.rmi.Remote 接口
    String sayHello() throws RemoteException; // 接口中所有方法必须抛出 RemoteException 异常, 因为远程调用缺乏可靠性,总是存在失败的可能。
}

接口的实现类以及服务端代码

这里方便起见将远程对象接口的实现类和服务端代码放在一起,实际上已经分开,这不是重点

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello{

    public Server(){super();};
    
    //The implementation class Server implements the remote interface Hello,
    //providing an implementation for the remote method sayHello. The method sayHello does not need to declare that it throws any exception because the method implementation itself does not throw RemoteException nor does it throw any other checked exceptions. 
    @Override
    public String sayHello() {
        return "Hello, world";
    }

    public static void main(String[] args){
        try {
            // 创建本机 1099 端口上的RMI registry
            Registry registry = LocateRegistry.createRegistry(1099);
            
            // jdk 1.5 以后利用 UnicastRemoteObject 动态生成 Stub
            // jdk 1.5 之前需要使用 rmic 命令来创建
            Server obj = new Server();
            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1099);
            // 有些RMI示例代码会让接口实现类直接继承自 UnicastRemoteObject ,效果是一样的,参见 UnicastRemoteObject 的构造函数,或则在接口实现类构造函数中做这个工作也可以

            // 将 Stub 绑定到RMI注册表中,方式多样,上文已经提过
            registry.bind("Hello", stub);
            
            System.err.println("Server ready");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

客户端代码

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    
    private Client(){
        
    }
    
    public static void main(String[] args){
        String host = (args.length < 1) ? null : args[0];
        String port = (args.length < 2) ? "1099" : args[1];
        try{
            // 获取 host 主机 port 端口上的 RMI 注册表引用,参数是程序运行参数
            Registry registry = LocateRegistry.getRegistry(host, Integer.parseInt(port));
            // 获取远程对象存根,方式多样,上文已经提过
            Hello stub = (Hello) registry.lookup("Hello");
            // 调用远程方法
            String response = stub.sayHello();
            System.out.println("response: " + response);
        }catch(Exception e){
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
    
}

编译运行

Server 运行结果

Client 运行参数(在Eclipse中 Run configiurations 中设置,也可以在命令行界面直接指定

Client 运行结果

在不同主机上部署RMI程序

服务端代码的更改(添加SecurityManager)

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello{

    public Server(){super();};
    
    @Override
    public String sayHello() {
        return "Hello, world";
    }

    public static void main(String[] args){
        
        //!!! 新添加了securityManageer !!!
        if(System.getSecurityManager() == null){
            System.setSecurityManager(new SecurityManager());
        } 
         
        try {
            // 创建本机 1099 端口上的RMI registry
            Registry registry = LocateRegistry.createRegistry(1099);
            
            // jdk 1.5 以后利用 UnicastRemoteObject 动态生成 Stub
            // jdk 1.5 之前需要使用 rmic 命令来创建
            Server obj = new Server();
            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1099);
            // 有些RMI示例代码会让接口实现类直接继承自 UnicastRemoteObject ,效果是一样的,参见 UnicastRemoteObject 的构造函数,或则在接口实现类构造函数中做这个工作也可以
            
            // 将 Stub 绑定到RMI注册表中,方式多样,上文已经提过
            registry.bind("Hello", stub);
            
            System.err.println("Server ready");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

客户端代码的更改(添加SecurityManager)

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    
    private Client(){
        
    }
    
    public static void main(String[] args){
        //!!! 新添加了securityManageer !!!
        if(System.getSecurityManager() == null){
            System.setSecurityManager(new SecurityManager());
        }

        String host = (args.length < 1) ? null : args[0];
        String port = (args.length < 2) ? "1099" : args[1];
        try{
            // 获取 host 主机 port 端口上的 RMI 注册表引用,参数是程序运行参数
            Registry registry = LocateRegistry.getRegistry(host, Integer.parseInt(port));
            // 获取远程对象存根,方式多样,上文已经提过
            Hello stub = (Hello) registry.lookup("Hello");
            // 调用远程方法
            String response = stub.sayHello();
            System.out.println("response: " + response);
        }catch(Exception e){
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
    
}

编辑一个安全策略文件 rmi.policy

grant {
    permission java.security.AllPermission;
};

这里为了避免任何麻烦赋予了所有代码所有权限,实际生产环境下应该仔细对于不同代码考虑各类权限的赋予,在这个示例中方便起见就这样吧。

部署

服务端需要的文件

  1. Hello.class

  2. Server.class

  3. rmi.policy

最后在服务端主机(win8系统 cmd 命令栏中,文件分布)

客户端需要的文件

  1. Hello.class

  2. Client.class

  3. rmi.policy
    最后在客户端主机(unbuntu 系统,终端中,文件分布)

运行

服务端运行

客户端运行

总结

对于在不同主机上RMI应用的部署,我在学习过程中碰到了各种各样的问题,而上面的简单示例并不能说明其他一些很重要的问题,建议大家查看官方FAQ,这里也给出一些比较有参考价值的链接:
JDK1.5之前做法
讲到了jdk1.5后的一些东西
另外建议大家还是好好看看官方文档,放弃度娘(根本没有有效信息),掏点钱用VPN吧!!!

文章评论