如果所有組件都在同一台電腦的同一個Java虛擬機的同一個堆空間上執行是最簡單的,但實際中我們面對的往往不是如此單一的情況,如果用戶端只是個能夠執行Java的裝置怎麼辦?如果為了安全性的理由只能讓伺服器上的程式存取資料庫怎麼辦?
我們知道,大多數情況下,方法的呼叫都是發生在相同堆上的兩個物件之間,如果要呼叫不同機器上的物件的方法呢?
通常,我們從某一台計算機上面取得另一台計算機上的信息是通過socket的輸入/輸出流,打開另一台計算機的socket連接,然後獲取outputStream來寫入數據.但如果要調用另一台電腦上,另一個Java虛擬機器上面的物件的方法你?我們當然可以自己定義和設計通信協議來調用,然後通過Socket把執行結果再傳回去,並且還能夠像是對本機的方法調用一樣,也就是說想要調用遠程的對象(像是別的堆上的),卻又要像是一般的調用.
這就是RMI帶給我們的功能.
遠程過程呼叫的設計
要創建出4種東西:伺服器、客戶端、伺服器輔助設施和客戶端輔助設施.
1.創建客戶端和服務端應用程式,伺服器應用程式時個遠端服務,是個帶有客戶端會呼叫的方法的對象
2.創建客戶端和伺服器端的輔助設施(helper)他們會處理所有客戶端和伺服器的底層網路輸入/輸出細節,讓客戶端和程式好像在處理本地呼叫一樣.
輔助設施的任務輔助設施是個在實際上執行通信的對象,他們會讓客戶端感覺上好像是在調用本機對象,客戶端對像看起來像是在調用遠程的方法,但實際上它只是在調用本地處理Socket和流媒體細節的代理.在伺服器這端,伺服器的輔助設施會透過socket連接來自客戶端設施的要求,解析打包送來的信息,然後調用真正的服務,因此對服務對象來說此呼叫來自本地.服務的輔助設施取得回傳值之後就把它包裝然後送回去(透過socket的輸出流)給客戶端的輔助設施.客戶端的輔助設施會解開這些資訊傳送給客戶端的對象
呼叫方法的過程
1.客戶端物件對輔助設施物件呼叫doBigThing()
2.客戶端輔助設施把呼叫資訊打包透過網路送到伺服器的輔助設施
3.服務端的輔助設施解開來自客戶端輔助設施的資訊,並以此調用真正的服務.
這個過程的描述圖如下:
JavaRMI提供客戶端和伺服器端的輔助設施對象
在Java中,RMI已經幫我們創建好客戶端和伺服器端的輔助設施,它也知道如何讓客戶端輔助設施看起來像是真正的服務,也就是說,RMI知道如何提供相同的方法給客戶端調用.
此外,RMI有提供執行期間所需全部的基礎設施,包括服務的查詢以及讓客戶端能夠找到與取得客戶端的輔助設施(真正的服務代理人).
使用RMI時,無需編寫任何網路或輸入/輸出的程式,客戶端對遠端方法的呼叫就跟對同一個Java虛擬機上的方法呼叫是一樣的.
一般呼叫和RMI呼叫有一點不同,雖然對客戶端來說,此方法調用看起來像是本地的,但是客戶端輔助設施會通過網絡發出調用,此調用最終還是會涉及到socket和流,一開始是本機呼叫,代理會把它轉成遠端的.中間的資訊是如何從Java虛擬機送到Java虛擬機器要看輔助設施物件所用的協定而定.
使用RMI時,必須要決定協議:JRMP或IIOP,JRMP是RMI原生的協議,它是為Java間的遠程調用而設計的,另外一方面,IIOP是為了CORBA而產生的,它讓我們能夠調用Java物件或其它類型的遠端方法,CORBA通常比RMI麻煩,因為若兩端不全都是Java的話,就會產生一堆可怕的轉譯和交談操作.
我們只關心Java對Java的操作,所以會使用相當簡易的RMI.
在RMI中,客戶端的輔助設施稱為stub,而伺服器端的輔助設施稱為skeleton.
如何建立遠端服務
1.建立Remote接口
遠端的介面定義了客戶端可以遠端呼叫的方法,它是個作為服務的多態性類別.stub和服務都會實現此接口
2.實作Remote接口
這個是真正執行的類別,它實現出定義在該介面上的方法,它是客戶端會呼叫的對象
3.用rmic產生stub和skeleton
客戶端和伺服器都有helper,我們無需創建這些類別或產生這些類別的源代碼,這都會在執行JDK所附的rmic工具時自動地處理掉
4.啟動RMIregistry(rmiregistry)
rmiregistry就像電話薄,用戶會從此處取得代理(客戶端的stub/helper物件)
5.啟動遠端服務
必須讓服務物件開始執行,實現服務的類別會起始服務的實例並向RMIRegistry註冊,要有註冊後才能對使用者服務.
服務端代碼
定義介面
/**
*
* MyRemote.java
*
* 功能: TODO
* 類別名稱: MyRemote.java
*
* ver 更日角色擔當者更內容
* ──────────────────────────────────────────────
* V1.00 2013-3-19 模組蘇若年初版
*
* Copyright (c) 2013 dennisit corporation All Rights Reserved.
*
* Email:<a href="mailto:[email protected]">發送郵件</a>
*
*
* Remote是個標記性的介面,意味著沒有方法,然而它對RMI有特殊的意義,所以必須遵守這項規則,
* 注意這裡用的是extends,介面是可以繼承其他介面的
*
*/
public interface MyRemote extends Remote{
/**
* 遠端的介面定義了客戶端可以遠端呼叫的方法,它是作為服務的多態性類別,也就是說,客戶端會
* 調動有實現此介面的stub,而此stub因為會執行網路和輸入/輸出工作,所以可能會發生各種
* 問題,客戶端鼻息處理或聲明異常來認知這一類風險,如果該方法在接口中聲明異常,調用該方
* 法的所有程序都必須處理或再聲明此異常.
*
* 遠端方法的參數和回傳值必須是primitive或serializable的.任何遠端方法的參數都會被
* 打包透過網路傳送,而這時透過序列化完成的,回傳值也是一樣.所以,如果使用的是自訂型別
* 時,必須對其序列化
* @return
* @throws RemoteException
* 所有介面中的方法都必須宣告RemoteException
*/
public String sayHello() throws RemoteException;
}
/**
*
* MyRemoteImpl.java
*
* 功能: TODO
* 類別名稱: MyRemoteImpl.java
*
* ver 更日角色擔當者更內容
* ──────────────────────────────────────────────
* V1.00 2013-3-19 模組蘇若年初版
*
* Copyright (c) 2013 dennisit corporation All Rights Reserved.
*
* Email:<a href="mailto:[email protected]">發送郵件</a>
*
* 為了要成為遠端服務物件,物件必須要有與遠端有關的功能,其中最簡單的方法就是繼承UnicastRemoteObject
* (來自java.rmi.server)讓這個父類別處理這些工作
*
*/
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
/**
* 父類別的建構子宣告了異常,所有你必須寫出建構子,因為它代表你的建構子會呼叫有風險的程式碼
*
* UnicastRemoteObject有個小問題,它的建構子會拋出RemoteException.處理它的唯一方式就是
* 對自己的實作宣告一個建構,如此才會有地方可以宣告出RemoteException.當類別被初始化的時候,父類
* 的建構子一定會被呼叫,如果父類別的建構子拋出異常,我們也必須聲明的自訂的建構子會拋出異常
* @throws RemoteException
*/
protected MyRemoteImpl() throws RemoteException {
}
/**
* 實作出介面所有的方法,但無須宣告RemoteException
*/
@Override
public String sayHello(){
return "server says, rmi hello world !";
}
public static void main(String[] args) {
try {
/**
* 我們已經有了遠端服務,還必須要讓遠端使用者存取,這可以透過將它初始化並加進RMI Registry
* (它一定要運作起來,不然此程式就會失敗).當註冊物件時,RMI系統會把stub加到registry中,
* 因為這是客戶端所需要的.使用java.rmi.Naming的rebind()來註冊服務
*/
MyRemote service = new MyRemoteImpl();
/**
* 建立出遠端物件,然後使用靜態的Naming.rebind()來產生關聯,所註冊的名稱會提供客戶端查詢
*/
Naming.rebind("Remote Hello World", service);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void exec(){
try {
/**
* 客戶端必須取得stub物件,因為客戶端必須要呼叫它的方法.這就得靠RMI registry了.客戶端會像查詢電話
* 簿一樣搜尋,找出上面有相符的名稱的服務.
* 用戶端查詢RMIRegistry,傳回stub對象
* Naming.lookup("rmi://127.0.0.1/Remote Hello World");
* 參數說明
* rmi://127.0.0.1/Remote Hello World
* 127.0.0.1表示主機名稱或主機IP位址
* Remote Hello World必須跟註冊的名稱一樣
*
*/
MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/Remote Hello World");
String tmp = service.sayHello();
System.out.println(tmp);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new MyRemoteClient().exec();
}
}
伴隨JDK而來的rmic工具會以服務的實作產生2個心的類stub和skeleton.它會按照命名規則在遠端實作名稱後面加上_Stub或_Skeleton。 rmic有幾個選項,包括了不產生skeleton、觀察產生出類的源代碼或使用IIOP作為通訊協議等.產生出的類會放在當前目錄下,要記住rmic必須能夠找到所實現的類,因此可能要從實作所在的目錄執行rmic(實際上可能需要考慮到包目錄結構和完整名稱,為了簡便這裡沒有運用到包)
呼叫命令列來啟動rmiregistry,要確定是從可以訪問到該類的目錄來啟動,最簡單的方法就是從類這個目錄來運行.
運行截圖如下
注意:
客戶端是使用介面來呼叫stub上的方法,客戶端的Java虛擬機器必須要有stub類別,但客戶端不會在程式碼中引用到stub類別,客戶端總是透過介面來操作真正的遠端對象
伺服器上必須要有stub和skeleton,以及服務與遠端的介面,它會需要stub類別是因為stub會被代換成連接在RMIRegistry上真正的服務.
使用RMI時常犯的錯誤:
1.忘記在啟動遠端服務錢啟動rmiregistry(使用Naming.rebind()註冊服務前rmiregistry必須啟動)
2.忘記把參數和回傳型別做成可序列化(編譯不會偵測到,執行時才會發現)
3.忘記將stub類別交給客戶端
RMI很適合編寫並運行遠端服務,但我們不會單獨使用RMI來執行網站服務,對大型的企業級應用程式來說,我們需要更多更好的功能.像交易管理、大量並發處理、安全性和資料庫管理等。這就需要用到EnterpriseApplicationServer.
JavaEE伺服器包含了Web伺服器與EnterpriseJavaBeans(EJB)伺服器.EJB伺服器作用於RMI呼叫與服務層之間.
RMI在JINI中的應用
Jini也是使用RMI(雖然也可以用別的協議),但多了幾個關鍵功能.
1.自適應探索(adaptivediscovery)
2.自恢復網路(self-healingnetworks)
RMI的客戶端得先取得遠端服務的位址和名稱.客戶端的查詢程式碼就要帶有遠端服務的IP位址或主機名稱(因為RMIRegistry就在上面)以及服務所註冊的名稱
但是用JINI時,用戶只需要知道一件事,服務所實現的介面!這樣就行.
Jini是用lookupservice,該查詢服務比RMIRegistry更強更有適應性.因為Jini會在網絡上自動的廣告.當查詢服務上線是,它會使用IP組播技術發送信息給整個網絡.不止這樣,如果客戶端在查詢服務已經廣播之後上線,客戶端也可以發出訊息給整個網路來詢問.
當服務上線時,它會動態的探索網絡上的JINI查詢服務併申請註冊,註冊時,服務會送出一個序列化的對象給查詢服務,此對象可以是RMI遠程服務的stub、網絡裝置的驅動程序,甚或是可以在客戶端執行的服務本身.並且註冊的是所實現的接口.而不是名稱.
自適應探索的運作
1.Jini查詢服務在網路上啟動,並使用IP組播技術為自己做宣傳
2.已經啟動的另外一個Jini服務會尋求向剛啟動的查詢服務註冊.它註冊的是功能而不是名稱,也就是所實現的接口,然後送出序列化對象給查詢服務
3.網路客戶想要取得實作ScientificCalculator的東西,可是不知道哪裡有,所以就問查詢服務
4.查詢服務回應查詢的結果
自恢復網路的運作
1.某個Jini服務要求註冊,查詢服務會給一份租約,新註冊的服務必須要定期更新租約,不然查詢服務會假設此服務已經離線了,查詢服務會力求呈現精確完整的可用服務網絡狀態
2.因為關機所以服務離線,因此沒有更新租約,查詢服務就把它踢掉.