JAVA RMI

譯自 The Java Tutorials (TraIls: RMI) 


RMI應用概述

一個RMI應用通常由兩部分組成,即客戶端和服務器端。服務器端程序創建若干遠程對象(Remote Objects),使這些遠程對象對客戶端是可訪問的,並等待客戶端來調用這些遠程對象的方法;客戶端程序則獲取服務器上若干個遠程對象的引用,並調用這些其方法。像這種類型的應用被稱爲Distributed Object Application


一個Distributed Object Application通常需要完成下列事情:

  • 定位遠程對象
  • 與遠程對象進行通訊
  • 爲被傳遞的對象加載其類定義

下圖展示了一個RMI應用如何利用RMI註冊服務(RMI Registry)來獲得對一個遠程對象的引用。Server令registry將一個名字與一個遠程對象綁定,client則通過其名字在server的registry中尋找該遠程對象並調用其上的方法。從圖中還可以看出,該RMI應用系統利用了一個已有的Web Service來爲在client與server之間傳遞的遠程對象加載類定義。





遠程接口、對象與方法

當某個對象實現了一個具備如下特性的遠程接口(Remote Interface)時,該對象就成爲了遠程對象(Remote Objects):

  • 該遠程接口擴展了接口java.rmi.Remote;
  • 該遠程接口中的每個方法都要在throws中聲明拋出java.rmi.RemoteException異常;


stub扮演了遠程對象的本地代表(或者說是代理)的角色,作爲client對遠程對象的引用。client調用本地stub上的方法,而該stub則負責調用遠程對象上的方法。



利用RMI創建分佈式應用

利用RMI創建一個分佈式應用包含如下四步:

  • 設計並實現該分佈式應用中的各個組件
  • 編譯源碼
  • 確保類可以通過網絡訪問
  • 啓動該應用

設計並實現分佈式應用中的各個組件

首先,確定該分佈式應用的架構,包括哪些組件是local objects,哪些組件是remote accessible。具體而言:

  • 定義遠程接口:客戶端是對遠程接口編程的,所以需要確定遠程接口中各個方法的參數及返回值的類型,
  • 實現遠程對象:遠程對象必須實現一個或者多個遠程接口
  • 實現客戶端代碼:使用遠程對象的客戶端可以在遠程接口被定義之後再定義

編譯源代碼

使用javac命令來編譯源文件。源文件包含了遠程接口的聲明、它們的定義,以及其他server及client端的實現。

注意:在Java 5 之前,還需要rmic來生成stub類。從Java 6 開始這一步已經不需要了。


確保類可以通過網絡訪問


啓動分佈式應用

啓動應用包括運行RMI遠程對象註冊服務(registry)、server及client。




編寫RMI Server

本文將構建一個稱之爲計算引擎的分佈式應用。該計算引擎接受來自client的任務請求,運行該任務,並返回計算結果。

Server端的代碼由接口和類組成:接口定義了client可以調用的方法,另外,接口也從client的視野定義了remote objects。



定義遠程接口

下面,我們定義接口compute.Compute,它代表了計算引擎自身。再定義接口compute.Task,

package compute;

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

public interface Compute extends Remote {
	<T> T executeTask(Task<T> t) throws RemoteException;
}
package compute;

public interface Task<T> {
	public T execute();
}

RMI利用Java序列化機制在不同的虛擬機之間以值傳遞(by value)的方式來傳遞對象。由於executeTask方法的返回值的類型爲Task,因此實現Task接口的類必須同時實現Serializable接口。


實現遠程接口

一般來說,一個實現了遠程接口的類應該定義自己的構造函數。這是因爲RMI Server需要創建初始的遠程對象並將其導出到RMI運行時環境,以使遠程對象能夠接受遠程調用。在該過程中,需要做下列事情:

  • 創建並安裝security manager
  • 創建並導出一個或者多個遠程對象
  • 在RMI Registry中註冊至少一個遠程對象(也可以使用其他naming service,例如JDNI)


下面給出ComputeEngine代碼

package engine;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;

public class ComputeEngine implements Compute  {	
	public ComputeEngine() {
		super();
	}

	@Override
	public <T> T executeTask(Task<T> t) throws RemoteException {
		return t.execute();
	}

	public static void main(String[] args) {
		if (System.getSecurityManager() == null)
			System.setSecurityManager(new SecurityManager());		
		try {
			Compute engine = new ComputeEngine();
			Compute stub = (Compute)UnicastRemoteObject.exportObject(engine, 0);
			Registry registry = LocateRegistry.getRegistry();
			registry.rebind("TheCompute", stub);
			System.out.println("ComputeEngine bound");
		} catch (RemoteException ex) {
			System.out.println("ComputeEngine exception!");
			ex.printStackTrace();
		}
	}
}

現在我們將對ComputeEngine類的各個部分作出解釋

實現每個遠程方法

ComputeEngine類實現了executeTask方法,該方法實現了ComputeEngine遠程對象與client之間的協議。每一個client向ComputeEngine提供一個Task對象(實現了Task接口中的execute方法),ComputeEngine運行每一個client的task,並將結果直接返回給client。


在RMI中傳遞對象

遠程方法的參數或者返回值幾乎可以是任何類型,包括本地對象、遠程對象以及Java基本數據類型。如果是本地對象,則必須實現java.io.Serializable接口。

關於遠程方法的參數和返回值:

  • 遠程對象實際上是按引用傳遞的:對一個遠程對象的引用是一個stub,這是一個client端的proxy,它實現了遠程對象所實現的所有接口
  • 本地對象使用序列化來按值傳遞:在默認情況下,除了static和transient成員,所有的類成員在傳遞時都會被複制

按引用傳遞遠程對象,意味着遠程方法對遠程對象的狀態所做的改變,都將在該原始的遠程對象中反映出來。

按值傳遞非遠程對象,意味着在接收者的Java VM中將創建非遠程對象的副本。這樣,接收者對該對象狀態所做的任何修改都無法發送者的原始對象中反應出來。同樣,發送者對該對象的修改對接收者也是不可見的。


實現Server的main主函數

我們在主函數中啓動了ComputeEngine,並等待接收client的調用請求。


創建並安裝security manager

創建並安裝security manager,可以防止不被信任的代碼訪問系統資源。security manager可以決定下載的代碼是否可以訪問本地文件系統或者做其他需要權限的操作。如果沒有安裝security manager,那麼RMI將不會爲遠程方法的參數或者返回值下載它們的類定義代碼。


確保遠程對象對客戶端是可見的

在代碼的第24-25行,我們創建了ComputeEngine的一個實例並將其導出到RMI運行時環境中。UnicastRemoteObject.exportObject方法負責將指定的遠程對象導出,使其能夠接受來自遠程客戶端對該遠程對象上的遠程方法的調用請求。


該方法的第二個參數(int類型),指定使用哪個TCP端口來監聽遠程調用請求。一般使用0,這將指定使用一個匿名端口,而實際的端口將在運行時由RMI或者操作系統來選擇。當然,也可以使用非零值來指定一個特定的監聽端口。


一旦UnicastRemoteObject.exportObject方法成功返回,則表示ComputeEngine已經準備就緒,並能夠隨時處理遠程的調用請求。


UnicastRemoteObject.exportObject方法返回被導出遠程對象的一個stub。


如果通訊資源有問題,例如指定的監聽端口已被佔用,那麼試圖導出遠程對象可能會拋出RemoteException異常。


爲了能夠獲得對其他遠程對象的引用,系統提供了一種特別類型的遠程對象——RMI registry。RMI registry是一種remote object naming service,它使得client能夠通過名字來獲取對遠程對象的引用。


利用java.rmi.registry.Registry接口,我們可以在registry中綁定、註冊或者查找遠程對象。一旦一個遠程對象通過RMI registry在本機上完成了註冊,任何主機上的client都可以通過名字來查找該遠程對象、獲得對它的引用並調用它的方法。registry可以由一個主機上的所有server共享,而某個server也可以創建並使用它自己的registry。


關於LocateRegistry.getRegistry()方法:

  • 使用無參數的版本,則會在本機的默認端口1099上創建一個對registry的引用,如果想在其它端口上創建對registry的引用,那麼需要使用帶int參數的LocateRegistry.getRegistry方法來指定端口
  • When a remote invocation on the registry is made, a stub for the remote object is passed instead of a copy of the remote object itself. Remote implementation objects, such as instances of ComputeEngine, never leave the Java virtual machine in which they were created. Thus, when a client performs a lookup in a server's remote object registry, a copy of the stub is returned. Remote objects in such cases are thus effectively passed by (remote) reference rather than by value.
  • For security reasons, an application can only bind, unbind, or rebind remote object references with a registry running on the same host. This restriction prevents a remote client from removing or overwriting any of the entries in a server's registry. A lookup, however, can be requested from any host, local or remote

一旦server通過本地RMI registry完成了註冊,它就打印出成功的信息(在我們的代碼中),main函數到此結束。在這裏,並不需要使用一個線程來保證server一直處於alive的狀態。只要其他虛擬機中存在一個對ComputeEngine的引用,不論是本地的還是遠程的,那麼該ComputeEngine對象就不會被關閉,也不會被垃圾回收掉。因爲我們的程序在registry中將一個引用綁定到了ComputeEngine,因此RMI將使得ComputeEngine所在進程保持運行。該ComputeEngine對象將一直可以接收調用請求,直到它在registry中被解除綁定並且沒有任何遠程client持有對ComputeEngine的遠程引用。




編寫客戶端程序

客戶端的程序比ComputeEngine要複雜:客戶端不僅需要調用ComputeEngine,還要定義將被ComputeEngine執行的任務的代碼。

在本例中,客戶端程序由兩部分組成。第一個部分是ComputePi類,它負責查找並調用一個Compute遠程對象;第二個部分是Pi接口,它實現了Task接口並定義了將被ComputeEngine執行的任務。


package pi;

import java.math.BigDecimal;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import compute.Compute;

public class ComputePi {
	public static void main(String[] args) {
		if (System.getSecurityManager() == null)
			System.setSecurityManager(new SecurityManager());
		try {
			String host = "127.0.0.1";	// Compute對象運行時所在的遠程主機的名字
			String precision = "10";	// 計算PI時期望的精度
			Registry registry = LocateRegistry.getRegistry(host); //獲取對遠程server上registry的引用
			Compute comp = (Compute) registry.lookup("TheCompute");
			Pi task = new Pi(Integer.parseInt(precision));
			BigDecimal pi = comp.executeTask(task);
			System.out.println(pi);
		} catch (Exception ex) {
			System.err.println(ex);
			ex.printStackTrace();
		}
	}
}
package pi;

import java.io.Serializable;
import java.math.BigDecimal;
import compute.Task;

public class Pi implements Task<BigDecimal>, Serializable {
	 private static final long serialVersionUID = 227L;

    /** constants used in pi computation */
    private static final BigDecimal FOUR =
        BigDecimal.valueOf(4);

    /** rounding mode to use during pi computation */
    private static final int roundingMode = 
        BigDecimal.ROUND_HALF_EVEN;

    /** digits of precision after the decimal point */
    private final int digits;
    
    /**
     * Construct a task to calculate pi to the specified
     * precision.
     */
    public Pi(int digits) {
        this.digits = digits;
    }

    /**
     * Calculate pi.
     */
    public BigDecimal execute() {
        return computePi(digits);
    }

    /**
     * Compute the value of pi to the specified number of 
     * digits after the decimal point.  The value is 
     * computed using Machin's formula:
     *
     *          pi/4 = 4*arctan(1/5) - arctan(1/239)
     *
     * and a power series expansion of arctan(x) to 
     * sufficient precision.
     */
    public static BigDecimal computePi(int digits) {
        int scale = digits + 5;
        BigDecimal arctan1_5 = arctan(5, scale);
        BigDecimal arctan1_239 = arctan(239, scale);
        BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
                                  arctan1_239).multiply(FOUR);
        return pi.setScale(digits, 
                           BigDecimal.ROUND_HALF_UP);
    }
    /**
     * Compute the value, in radians, of the arctangent of 
     * the inverse of the supplied integer to the specified
     * number of digits after the decimal point.  The value
     * is computed using the power series expansion for the
     * arc tangent:
     *
     * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + 
     *     (x^9)/9 ...
     */   
    public static BigDecimal arctan(int inverseX, 
                                    int scale) 
    {
        BigDecimal result, numer, term;
        BigDecimal invX = BigDecimal.valueOf(inverseX);
        BigDecimal invX2 = BigDecimal.valueOf(inverseX * inverseX);
        
        numer = BigDecimal.ONE.divide(invX, scale, roundingMode);

        result = numer;
        int i = 1;
        do {
            numer = 
                numer.divide(invX2, scale, roundingMode);
            int denom = 2 * i + 1;
            term = 
                numer.divide(BigDecimal.valueOf(denom),
                             scale, roundingMode);
            if ((i % 2) != 0) {
                result = result.subtract(term);
            } else {
                result = result.add(term);
            }
            i++;
        } while (term.compareTo(BigDecimal.ZERO) != 0);
        return result;
    }
}

與Server端一樣,Client端運行時必須首先安裝Security Manager,這是因爲在接收server端的遠程對象的stub時,client需要從server端下載代碼,而RMI在下載類代碼時必須運行Security Manager。

在安裝完Security Manager之後,Client使用綁定遠程對象時所用的名字(即TheCompute)來查找Compute遠程對象。然後,Client利用LocateRegistry.getRegistry這個API來獲取對server上registry的引用。這裏,我們在API中指定了registry所在的server所在主機的名字(默認端口爲1099)。如果使用其他的端口,需要在使用重載的API來指定行新端口。

接下來,Client創建了一個Pi對象,然後調用Compute遠程對象的executeTask方法,並將pi對象作爲executeTask方法的參數。下圖描述了消息在ComputePi客戶端、rmiregistry和ComputeEngine之間的流動情況。






Pi類實現了Task接口並計算pi的數值。這裏注意,任何可序列化的類,不論是直接地還是間接地實現了Serializable接口,都必須有一個名爲serialVersionUID的private static final field,以此來保證在不同版本之間的序列化的兼容性。 


在這個例子中,最神奇的地方在於:實現Compute接口的類(即ComputeEngine),直到executeTask方法被調用時,才需要得到PI類的實現代碼(通過將PI對象作爲參數傳遞給executeTask類)。在這個時候,PI的實現類代碼被RMI下載到ComputeEngine的JVM中,




編譯和運行RMI應用

既然Compute Engine例子相關的代碼都已經就緒,我們就要開始編譯並運行了。


編譯

在真實的應用場景中,開發者往往會創建一個包含了Compute接口和Task接口的JAR文件,來供client和server使用。在本節中,您將看到,client端的PI類將在運行時被下載到server。同時,Compute和Task接口將在運行時從server下載到registry。

本例中有如下幾個package:

  • compute - 包含ComputeTask接口
  • engine - 包含ComputeEngine的實現類
  • client - 包含ComputePi客戶端代碼以及Pi實現類


爲接口構建JAR文件

首先,我們來編譯compute包裏的代碼

假定我們代碼路徑爲:

     



cd D:\example
javac compute\Compute.java compute\Task.java
jar cvf compute.jar compute/*.class

可以看到,在D:\example路徑下新增了compute.jar文件

After you build either server-side or client-side classes with the javac compiler, if any of those classes will need to be dynamically downloaded by other Java virtual machines, you must ensure that their class files are placed in a network-accessible location. In this example, for Solaris OS or Linux this location is /home/user/public_html/classes because many web servers allow the accessing of a user'spublic_html directory through an HTTP URL constructed as http://host/~user/. If your web server does not support this convention, you could use a different location in the web server's hierarchy, or you could use a file URL instead. The file URLs take the form file:/home/user/public_html/classes/ on Solaris OS or Linux and the form file:/c:/home/user/public_html/classes/ on Windows. You may also select another type of URL, as appropriate.

The network accessibility of the class files enables the RMI runtime to download code when needed. Rather than defining its own protocol for code downloading, RMI uses URL protocols supported by the Java platform (for example, HTTP) to download code. Note that using a full, heavyweight web server to serve these class files is unnecessary. For example, a simple HTTP server that provides the functionality needed to make classes available for downloading in RMI through HTTP can be found at 
Also see Remote Method Invocation Home.


構建Server類

cd D:\example
javac -cp compute.jar engine/ComputeEngine.java

這樣,在目錄engine下,生成了ComputeEngine.class文件。



構建client類




運行

關於Security

不論是Server還是Client,它們運行時都需要安裝Security Manager。在運行server或者client時,我們需要指定一個安全策略文件(Security Policy File)來賦予代碼所需的權限。




































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章