概述
RMI 是 Java 提供的一個完善的簡單易用的遠程方法呼叫框架,采用客戶/服務器通信方式,在服務器上部署了提供各種服務的遠程物件,客戶端請求訪問服務器上遠程物件的方法,它要求客戶端與服務器端都是 Java 程式
RMI 框架采用代理來負責客戶與遠程物件之間通過 Socket 進行通信的細節,RMI 框架為遠程物件分別生成了客戶端代理和服務器端代理,位于客戶端的代理必被稱為存根(Stub),位于服務器端的代理類被稱為骨架(Skeleton)

當客戶端呼叫遠程物件的一個方法時,實際上是呼叫本地存根物件的相應方法,存根物件與遠程物件具有同樣的介面,存根采用一種與平臺無關的編碼方式,把方法的引數編碼為位元組序列,這個編碼程序被稱為引數編組,RMI 主要采用Java 序列化機制進行引數編組,存根把以下請求資訊發送給服務器:
- 被訪問的遠程物件的名字
- 被呼叫的方法的描述
- 編組后的引數的位元組序列
服務器端接收到客戶端的請求資訊,然后由相應的骨架物件來處理這一請求資訊,骨架物件執行以下操作:
- 反編組引數,即把引數的位元組序列反編碼為引數
- 定位要訪問的遠程物件
- 呼叫遠程物件的相應方法
- 獲取方法呼叫產生的回傳值或者例外,然后對它進行編組
- 把編組后的回傳值或者例外發送給客戶
客戶端的存根接收到服務器發送過來的編組后的回傳值或者例外,再對它進行反編組,就得到呼叫遠程方法的回傳結果
JDK5.0 之后,RMI 框架會在運行時自動為運程物件生成動態代理類(包括存根和骨架類),從而更徹底地封裝了 RMI 框架的實作細節,簡化了 RMI 框架的使用方式
創建 RMI 應用
創建一個 RMI 應用包括以下步驟:
- 創建遠程介面:繼承 java.rmi.Remote 介面
- 創建遠程類:實作遠程介面
- 創建服務器程式:負責在 RMI 注冊器中注冊遠程物件
- 創建客戶程式:負貴定位遠程物件,并且呼叫遠程物件的方法
1. 創建遠程介面
遠程介面中宣告了可以被客戶程式訪問的遠程方法,并直接或間接繼承 java.rmi.Remote 介面
import java.rmi.*;
public interface HelloService extends Remote {
public String echo(String msg) throws RemoteException;
}
2. 創建遠程類
遠程類必須實作一個遠程介面,此外,為了使遠程類的實體變成能為遠程客戶提供服務的遠程物件,可通過以下兩種途徑之一把它匯出為遠程物件:
-
使遠程類繼承 java.rmi.server.UnicastRemoteObjcct 類,并且遠程類的構構方法必宣告拋出 RemoteException
import java.rmi.*; import java.rmi.server.UnicastRemoteObjoct; public class HelloServlceImpl extends UnicagtRemoteObject implements HelloService { private String name; public HelloServicelmpl(String name) throws RemoteException { this.name = name; } public String echo(String msg) throws RemoteException { System.out.println(name + ":測用echo()方法"); return "echo;" + msg + " from" + name; } } -
如果一個遠程類已經繼承了其他類,無法再繼承 UnicastRemoteObiect 類,那么可以在構造方法中呼叫 UnicastRemoteObject 類的靜態 expotObject 方法,同樣,遠程類的構造方法也必須宣告拋出 RemoteException
public class HelloServlceImpl extends OtherClass implements HelloService { private String name; public HelloServicelmpl(String name) throws RemoteException { this.name = name; //引數 port 指定監聽的埠,如果取值為0,就表示監聽任意一個匿名埠 UnicagtRemoteObject.exportobject(this, 0); } public String echo(String msg) throws RemoteException { System.out.println(name + ":測用echo()方法"); return "echo;" + msg + " from" + name; } }
3. 創建服務器程式
RMI 采用一種命名服務機制來使得客戶程式可以找到服務器上的一個遠程物件,RMI注冊器提供這種命名服務,好比電話查詢系統,那些希望對外公開聯系方式的單位先到查詢系統登記,當客戶想知道某個單位的聯系方式時,只需向查詢系統提供單位的名字,查詢系統就會回傳該單位的聯系方式
啟動 RMI 注冊器有兩種方式,一種方式是直接運行 rmiregistry.exe 程式,在 JDK 的安裝目錄的 bin 子目錄下有一個 rmiregistry.exe 程式,它是提供命名服務的注冊器程式,盡管 rmiregistry 注冊器程式也可以單獨運行在一個主機上,但出于安全的原因,通常讓 rmiregistry 注冊器程式與服務器程式運行在同一個主機上
啟動 RMI 注冊器的另一種方式是在服務器程式中呼叫 java.rmiregistry.LocateRegistry 類的靜態方法 createRegistry()
//默認的監聽路口為1099
Registry registry = LocateRegistry.createRegigtry(1099);
向注冊器注冊遠程物件有三種方式:
//創建遠程物件
HelloService service1 = new HelloServiceImpl("service1");
//方式1:呼叫 java.i.registry.Registy 介面的 bind 或 rebind 方法
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("HelloService1", service1);
//方式2:呼叫命名服務類 java.rmi.Naming 的 bind 或 rebind 方法
Naming.rebind("HelloService1", service1);
//方式3:呼叫 JNDI API 的 javax.naming.Context 介面的 bind 或rebind 方法
Context namingContext = new InitialContext();
namingContext.rebind("rmi:HelloService1", service1);
下例的 SimpleServer 類創建了兩個 HelloServicelmpl 遠程物件,接著創建并啟動 RMI 注冊器,然后把兩個遠程物件注冊到 RMI 注冊器
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class SimpleServer {
public static void main( String args[]) {
try {
HelloService service1 = new HelloServiceImpl("service1");
HelloService service2 = new HelloServiceImpl("service2");
//創建并啟動注冊器
Registry registry = LocateRegistry.createRegistry(1099);
//注冊遠程物件
regigtry.rebind("HelloService1", service1);
regigtry.rebind("HelloService2", service2);
} catch(Exception e) {
e.printStackTrace();
}
}
}
關于向 RMI 注冊器注冊遠程物件,需要注意的是,遠程物件即使沒有在注冊器中注冊,也可被遠程訪問
4. 創建客戶程式
下例的 SimpleClient 類先獲得遠程物件的存根物件,接著呼叫它的遠程方法
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class SimpleClient {
public static void main(String args[]) {
try {
//回傳本地主機的RMI注冊器物件,引數port指定RMI注冊器監聽的埠
Registry registry = LocateRegistry.getRegistry(1099);
//查找物件,回傳與引數name指定的名字所系結的物件
//回傳的是一個名為"com.sun.proxy.$Proxy0"的動態代理類的實體
HelloService service1 = (HelloService) registry.lookup("HelloService1");
HelloService service2 = (HelloService) registry.lookup("HelloService2");
System.out.println(service1.echo("hello"));
System.out.println(service2.echo("hello"));
}
}
}
遠程方法中的引數與回傳值傳遞
當客戶端呼叫服務器端的遠程物件的方法時,客戶端會向服務器端傳遞引數,服務器端則會向客戶端傳遞回傳值,RMI 規范對引數以及回傳值的傳遞的規定如下所述:
- 只有基本型別的資料、遠程物件以及可序列化的物件才可以被作為引數或者回傳值進行傳遞
- 如果引數或回傳值是一個遠程物件,那么把它的存根物件傳遞到接收方,也就是說接收方得到的是遠程物件的存根物件
- 如果引數或回傳值是可序列化物件,那么直接傳遞該物件的序列化資料,也就是說接收方得到的是發送方的可序列化物件的復制品
- 如果引數或回傳值是基本型別的資料,那么直接傳遞該資料的序列化資料,也就是說,接收方得到的是發送方的基本型別的資料的復制品
分布式垃圾收集
在 Java 虛擬機中,對于一個本地物件,只要不被本地 Java 虛擬機內的任何變數參考,它就會結束生命周期,可以被垃圾回收器回收,而對于一個遠程物件,不僅會被本地 Java 虛擬機內的變數參考,還會被遠程參考
服務器端的一個遠程物件受到三種參考:
- 服務器端的一個本地物件持有它的本地參考
- 這個遠程物件已經被注冊到 RMI 注冊器,可以理解為,RMI 注冊器持有它的參考
- 客戶端獲得了這個遠程物件的存根物件,可以理解為,客戶端持有它的遠程參考
RMI 框架采用分布式垃圾收集機制來管理遠程物件的生命周期,當一個遠程物件不受到任何本地參考和遠程參考時,這個遠程物件才會結束生命周期,并且可以被本地 Java 虛擬機的垃圾回收器回收,
服務器端如何知道客戶端持有一個遠程物件的遠程參考呢?當客戶端獲得了一個服務器端的遠程物件的存根后,就會向服務器發送一條租約通知,告訴服務器自己持有這個遠程物件的參考了,客戶端對這個遠程物件有一個租約期限,默認值為 600000ms,當至達了租約期限的一半時間,客戶如果還持有遠程參考,就會再次向服務器發送租約通知,客戶端不斷在給定的時間間隔中向服務器發送租約通知,從而使腸務器知道客戶端一直持有遠程物件的參考,如果在租約到期后,服務器端沒有繼續收到客戶端的新的租約通知,服務器端就會認為這個客戶已經不再持有遠程物件的參考了
動態加載
遠程物件一般分布在服務器端,當客戶端試圖呼叫遠程物件的方法時,如果在客戶端還不存在遠程物件所依賴的類檔案,比如遠程方法的引數和回傳值對應的類檔案,客戶就會從 java.rmi.server.codebase 系統屬性指定的位貿動態加載該類檔案
同樣,當服務器端訪問客戶端的遠程物件時,如果服務器端不存在相關的類檔案,腐務器就會從 java.rmi.server.codebase 屬性指定的位置動態加載它們
此外,當服務器向 RMI 注冊器注冊遠程物件時,注冊器也會從 java.rmi.server.codebase 屬性指定的位置動態加載相關的遠程介面的類檔案
前面的例子都是在同一個 classpath 下運行服務器程式以及客戶程式的,這些程式都能從本地 classpath 中找到相應的類檔案,因此無須從 java.rmi.server.codebase 屬性指定的位置動態加載類,而在實際應用中,客戶程式與服務器程式運行在不同的主機上,因此當客戶端呼叫服務器端的遠程物件的方法時,有可能需要從遠程檔案系統加載類檔案,同樣,當服務器端呼叫客戶端的遠程物件的方法時,也有可能從遠程檔案系統加載類檔案
我們可以且把這些需要被加載的類的檔案都集中放在網路上的同一地方,啟動時將java.rmi.server.codebase 設定為指定位置,從而實作動態加載
start java -Djava.rmi.server.codebase=http://www.javathinker.net/download/
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555245.html
標籤:其他
下一篇:返回列表
