牛刀小试Java Remote Method Invocation (RMI) 远程方法调用

来源:互联网 发布:p2p软件制作 编辑:程序博客网 时间:2024/05/05 02:10

绪言:本文将简单介绍Java RMI,并用一个简单例子来阐述RMI的整个工作过程。

简介

在以前,由于要考虑硬件,操作系统,语言等等问题,开发一些跨平台,分页式计算的程序是非常困难的。如今利用Java完成同样的工作,将会是变得异常简单,而且从来都不用去担心平台问题。

源于JDK 1.1或更高版本的Java RMI开发包,是Java中分页式计算的接口。

分布式RMI应用由两部分组成.

1) RMI服务器-----包含需要被远程调用的方法。服务器创建几个远程对象,并在RMI注册表中产生它们的引用(也就是用远程对象的唯一名注册,RMI注册是在RMI服务器上运行的服务),客户端只要得到这个远程对象的引用就可以调用服务器中的那些方法了。

2) RMI客户


如下图,整个Java RMI流程由三层组成:

RMI Illustration

第一层是 桩/骨架层 Stub/Skeleton,负责管理控制本地和远程的对象接口。

第二层是远程调用层RRL(Remote Reference Layer),RRL在客户端和服务器端都有,客户的RRL接受Stub的请求,组包(就是把数据转换为可在网络上传输的格式)之后,发送到服务器RRL,再拆包,传给Skeleton。

第三层是传输层,负责客户RRL和服务器RRL的连接,是基于TCP/IP的连接。

下面进入正题。

1) 定义并创建远程接口类
2) 创建远程服务器实现类
3) 生成Stub和Skeleton
4) 创建客户端程序
5) 创建服务器端程序
6) 创建安全策略
7) 编译例子
8) 运行例子


本文中例子是用来生成分期还款计划的程序,客户端向服务器发送贷款的金额和还款期数,申请一个 schedule 对象到本地。在服务器端,将接收金额和还款期数,利用自己已有的利息倍数,初始化一个当地的 schedule 对象,并返回给客户端。继后,客户端便可以打印并修改该对象。然而,客户端拥有的仅是该对象的一个副本,并没有改动服务器端的对象。下图描述了在RMI调用过程中,各个class充当的角色。

定义并创建远程接口类

开发RMI程序要做的第一件事,便是要创建远程接口。接口定义了哪些方法及成员变量将要从远程对象中获取。

远程接口类必须引入 RMI 包, 每一个远程调用的方法,都要抛出RMI远程异常(java.rmi.RemoteException),用来管理调用时的错误。下面是我们例子中的远程接口定义程序:mathCalc.java

  /****************************************************
   * module: mathCalc.java
   ***************************************************
*/
  
import java.lang.*;
  
import java.rmi.*;

  
public interface mathCalc extends java.rmi.Remote
  {
     
public schedule amortizeSchedule( float ammount,
                                       
int duration ) throws
                                                 java.rmi.RemoteException;
     
public void     printRate() throws java.rmi.RemoteException;
  }

如果你熟悉Java及接口的话,你可以很容易就生成该接口的远程实现。

创建远程服务器实现类

当以上接口的定义完毕后,你将要定义该接口在服务器端实际要执行的代码。以下是该实现类:mathCalcImp.java

 /****************************************************
   * module: mathCalcImp.java
   ***************************************************
*/
  
import java.rmi.*;
  
import java.rmi.server.*;

  
class mathCalcImp extends UnicastRemoteObject implements mathCalc
  {
     
float interestRate = (float)7.5;

     mathCalcImp() 
throws java.rmi.RemoteException
     {
     }

     
public schedule amortizeSchedule( float ammount, int duration ) throws
                                       java.rmi.RemoteException
     {
        System.out.println(
"Amortizeing Schedule.");
        
returnnew schedule( interestRate, ammount, duration ) );
     }

     
public void printRate() throws java.rmi.RemoteException
     {
        System.out.println(
"Current Interest Rate is " + interestRate );
     }
  }

请留意这个实现类中引入了两钟包:java.rmi.* 和 java.rmi.server.*。这样,就可以继承UnicastRemoteObject类,用来支持客户端的连接。这个类在RMI中,是用来管理客户端/服务器端之间的点到点的连接。

留意这个类是如何实现我们之前定义的mathCalc接口,同样,每个方法都要抛出RMI远程异常。

对象 Serialization
amortizeSchedule() 方法打印出消息,并初始化一个schedule对象,用于返回给客户端。期间,schedule对象将被序列化,并排列好,由数据流的形式传送回客户端。

在讨论序列化之前,先来看看schedule对象。


  
/****************************************************
   * module: schedule.java
   ***************************************************
*/
  
import java.lang.*;
  
import java.util.*;
  
import java.io.*;

  
class schedule implements Serializable
  {
    
float  totalLoanAmt;
    
float  usrAmmount;
    
float  interestRate;
    
int    loanDuration;

    schedule( 
float rate, float ammount, int duration )
    {
       interestRate 
= rate;
       usrAmmount   
= ammount;
       loanDuration 
= duration;
       totalLoanAmt 
= ammount + (ammount / rate);
    }

    
void print()
    {
       System.out.println(
"Schedule Created.");
       System.out.println(
"Calculation information based on:");
       System.out.println(
"            Rate        [%" + interestRate + "]" );
       System.out.println(
"            Ammount     [$" + usrAmmount + "]" );
       System.out.println(
"            Duration    [ " + loanDuration + "]" );
       System.out.println(
"            Total Loan  [$" + totalLoanAmt + "]" );

       
int   couponNum        = 0;
       
float balanceRemaining = totalLoanAmt;
       
float monthlyPayment   = 0;

       System.out.println();
       System.out.println( 
"Payment Monthly Payment Ammount   Balance Remaining");
       System.out.println( 
"------- -----------------------   -----------------");

       
while( balanceRemaining > 0 )
       {
          couponNum
++;
          monthlyPayment 
= totalLoanAmt/loanDuration;
          
if( balanceRemaining < monthlyPayment )
          {
            monthlyPayment   
= balanceRemaining;
            balanceRemaining 
= 0;
          }
          
else
          {
            balanceRemaining 
= balanceRemaining - monthlyPayment;
          }

          System.out.println( couponNum 
+ " " + monthlyPayment + " " +
                              balanceRemaining );
       }
    }
  }

如果你要通过远程接口传送一个当地对象,你必须将比对象序列化。在上面程序中,请留意schedule对象实现了serializable接口,但我们并不能看见任何序列化的代码。这是因为Java在serializable接口中已经帮你完成了对象的序列化。如果你的对象实现的是externalizable接口,而不是serializable接口,那么你必须在schedule.java对象中添加两个方法,serialize和deserialize。即是schedule对象要自己完成对内部数据的序列化。如果你传送一个没有实现serializeable/externalizeable接口的对象, Java将会抛出整理异常。
注意: 请小心使用序列化,因为当Java序列化一个对象的时候,它会将对象中的所有东西都“压扁”,包括子类,内部类等。如,请不要尝试序列化一个你的硬盘,因为在序列化的过程中,将会有相当大的开销。


生成Stub和Skeleton

现在接口和实现类都已经做好,便可以生成stub和skeleton的代码了。我们可以借助于JDK提供的rmic编译器,以下命令就可以生成stub和skeleton的.class文件。但它不会自动生成.java源文件,如果你想查看它的java代码,在编译的时候你可以加上 -keep 选项。这些java源文件,研究一下可以,请老实点不要修改它们。

 > rmic mathCalcImp

运行rmic编译器后,你将会看到mathCalcImp_Skel.class和mathCalcImp_Stub.class。

创建客户端程序

现在我们将创建客户端程序,代码如下:calcClient.java.


  
/****************************************************
   * module: calcClient.java
   ***************************************************
*/
  
import java.util.*;
  
import java.net.*;
  
import java.rmi.*;
  
import java.rmi.RMISecurityManager;

  
public class calcClient
  {
      
public static void main( String args[] )
      {
         mathCalc cm 
= null;
         
int i = 0;
         System.setSecurityManager( 
new RMISecurityManager());

         
try
         {
            System.out.println(
"Starting calcClient");

            String url 
= new String( "//"+ args[0+ "/calcMath");

            System.out.println(
"Calc Server Lookup: url =" + url);
            cm 
= (mathCalc)Naming.lookup( url );

            
if( cm != null )
            {
              String testStr 
= "Requesting Current Interest Rate...";

              
// Print Current Interest Rate from the server
              cm.printRate();

              
// Amortize a schedule using the server interest
              
// rate.

              
float    amount   = (float)10000.50;
              
int      duration = 36;
              schedule curschd  
= cm.amortizeSchedule( amount, duration );

              
// Print the schedule
              curschd.print();
            }
            
else
            {
              System.out.println(
"Requested Remote object is null.");
            }
         }
         
catch( Exception e )
         {
            System.out.println(
"An error occured");
            e.printStackTrace();
            System.out.println(e.getMessage());
         }
      }
  }

连接远程的对象,你必须提供URL,URL由服务器端的地址,和你要请求的对象名称组成。服务器提供的请求服务,是通过rmiregistry来提供的。客户端程序则通过Naming.lookup方法请求服务器端的远程对象的引用(是reference而不是对象)。注意在得到了远程对象后,便会将它进行向上转型为本地的接口类型(mathCalc)。请求远程对象的URL形式如下:

  rmi://www.myserver.com/myObject
       或者
  //www.myserver.com/myObject

如果客户端成功获取远程对象的引用,便可以远程调用该对象的方法。如在本例中将会在服务器端打印出贷款的利息倍数。程序同时请求生成一个schedule对象,如果“分期还款计划”对象生成成功,将会在客户端本地生成一个该对象的副本,客户端的程序便可以像正常一样使用该对象。因为是一个副本,这个对象无论怎么被改动,服务器端都不会知道。概而述之,本地对象是副本,远程对象是一个引用。


创建服务器端程序

服务器程序和客户端程序相似,很简单。如下:calcServ.java


  
/****************************************************
   * module: calcServ.java
   ***************************************************
*/
  
import java.util.*;
  
import java.rmi.*;
  
import java.rmi.RMISecurityManager;

  
public class calcServ
  {
      
public static void main( String args[] )
      {
         System.setSecurityManager( 
new RMISecurityManager());

         
try
         {
            System.out.println(
"Starting calcServer");
            mathCalcImp cm 
= new mathCalcImp();

            System.out.println(
"Binding Server");
            Naming.rebind(
"calcMath", cm );
            System.out.println(
"Server is waiting");
         }
         
catch( Exception e )
         {
            System.out.println(
"An error occured");
            e.printStackTrace();
            System.out.println(e.getMessage());
         }
      }
  }


你可以注册很多对象,为简单起见,本例只注册了一个对象。


创建安全策略

如果不设定并使用安全策略文件,系统将提示错误:

java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)

本例中,rmi.policy是安全政策文件,表示该类执行的权限。rmi.policy文件包含下列内容:

grant{
    permission java.security.AllPermission;
};

这个安全政策文件关闭检查权限,允许所有访问,是我们不需要考虑Java 2安全模型的细节。显然,生产环境中应使用更严格的安全策略文件。

如把安全策略文件设计成

grant{
    permission java.net.SocketPermission "localhost:
1099", "connect, resolve";

};

编译例子

用以下命令编译客户端和服务器端程序:

  javac calcClient.java
  javac calcServ.java

运行例子

终于可以运行整个例子了。首先,我们得在服务器端启动rmiregistry(RMI注册系统),启动前,请检查你的CLASSPATH环境变量,确保registry能找到你的类。启动registry如下:

>start rmiregistry <port> (start可以另外重启一个进程,表现就是自动打开一个新的DOS窗口,port是可选的参数,默认是1099端口)

接着,启动服务器端程序。

>java -Djava.security.policy=rmi.policy calcServ

此时,服务器端启动并打印出如下信息:

Starting calcServer
Binding Server
Server is waiting


此时,客户端程序可以启动了。

>java -Djava.security.policy=rmi.policy calcClient www.myserver.com

此时你会看到请求进入服务器端,在服务器端打印出利息倍数,并返回一个远程对象的引用。客户端将打印出从服务器端返回的schedule对象的内容。


结论:
通过本例子的讲解,希望读者可以简单理解RMI的工作流程,并可以基于RMI写出功能强大的Java程序。

Ray HONG
19 Apr 2007

<Refer:Introduction to Java Remote Method Invocation (RMI)>

原创粉丝点击