Java遠程調用RMI(Remote Method Invocation)

1.RMI和Java Remote Method Invocation

在討論RMI之前,我們先看看網站的架構。典型的網站有一個服務提供商(Web Server),多個服務使用者(Web Client)。網站使用的是瀏覽器(Browser)作爲客戶端,也就是所說的BS架構(Browser-Server)。還有一種不是使用瀏覽器作爲客戶端的,比如說即時聊天工具(QQ、MSN)等,它們需要你在本地安裝相應的客戶端程序後才能運行,這就是所說的CS架構(Client-Server)。CS架構和BS架構相比較,BS的優勢在於它能夠有很好的移植和兼容性(只要你本地安裝了瀏覽器,原則上你就能使用我的系統);而對於CS架構,個人覺得它的優勢在於可操作性(客戶端完全由自己開發,可以想怎麼寫就怎麼寫,完全不需要受到瀏覽器的約束)。
現在我們說說所謂的RMI,RMI全稱爲Remote Method Invocation,中文翻譯爲遠程方法調用。指的是從本地調用服務器上的一組方法,服務器根據提交的信息(方法名、方法攜帶參數)計算得到結果、並將結果返回給調用者。Java Remote Method Invocation指的是在Java上實現上述機制。RMI可以理解成一種思想,一種架構,而JRMI則是這種思想或者架構的Java版本的實現。

2.淺談如何實現RMI機制

好的機制需要屏蔽底層的實現細節。對於RMI的實現,需要做到:調用遠程對象(Remote Object)和調用本地對象(Local Object)對於調用者來說沒有任何區別。基於這個出發點,我們看看本地調用如何實現。

2.1 調用本地對象

首先我們需要定義一個Java類,該類有一個方法localCall。
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.local;  
  2.   
  3. /** 
  4.  * 本地對象,用於被調用 
  5.  * @author Lethorld 
  6.  * 
  7.  */  
  8. public class LocalObject {  
  9.     /** 
  10.      * 本地調用,用於測試 
  11.      * @param arg 入參 
  12.      * @return 返回 
  13.      */  
  14.     public String localCall(String arg) {  
  15.         // TODO : do something.  
  16.         return arg;  
  17.     }  
  18. }  
然後,我們寫一個類用於調用這個本地對象。
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.local;  
  2.   
  3. /** 
  4.  * 本地調用者 
  5.  * @author Lethorld 
  6.  * 
  7.  */  
  8. public class LocalInvoker {  
  9.     /** 
  10.      * 主函數 
  11.      * @param args 入參 
  12.      */  
  13.     public static void main(String[] args) {  
  14.         LocalObject object = new LocalObject();  
  15.         String retn = object.localCall("Hello");  
  16.         System.out.println(retn);  
  17.     }  
  18. }  
這樣我們便完成了本地的一次對象調用。我們的運行main函數後,主程序會new一個LocalObject對象,並且調用該對象的localCall方法。localCall方法根據入參"Hello",返回相應的返回值。這裏面沒有做處理,所以返回的是Hello。
OK,這段代碼應該是所有隻要稍微懂一點Java的人都能夠看明白,並且很輕鬆地寫出來的。那麼,對於Java的遠程調用,也就是上面所說的RMI,是否可以簡單的把所有的local改成remote就OK了呢?答案是否定的。

2.2 定義與實現分離

要實現遠程調用,首先必須考慮的是如何將方法的定義和方法的實現分離開。如果方法定義與實現不能相互分離,那麼我們怎麼能夠將方法的實現放到服務器端,而僅僅在客戶端上保存一個方法的聲明呢?對於Java,要完成將定義與實現相分離的任務,我們自然而然地想到了接口(interface)這個工具。請看百度百科中對於Java接口的描述:“Java中的接口是一系列方法的聲明,是一些方法特徵的集合,一個接口只有方法的特徵沒有方法的實現,因此這些方法可以在不同的地方被不同的類實現,而這些實現可以具有不同的行爲(功能)。”你看,這難道不就是我們想要的所謂的定義與實現相分離的特性嗎?好吧,下面我們用接口來重寫上面的例子。
首先我們定以接口,接口的定義這裏不贅述,也不屬於本文準備詳細闡述的內容。這個接口的名字叫ILocalObject,它有一個方法的聲明叫localCall。注意localCall只是一個聲明,它沒有顯示它將會如何實現localCall這個操作。
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.local;  
  2.   
  3. /** 
  4.  * 本地調用接口 
  5.  * @author Lethorld 
  6.  * 
  7.  */  
  8. public interface ILocalObject {  
  9.     /** 
  10.      * 本地調用,用於測試 
  11.      * @param arg 入參 
  12.      * @return 返回 
  13.      */  
  14.     public String localCall(String arg);  
  15. }  
然後我們定義一個類LocalObjectImpl,該類實現了接口ILocalObject。方法localCall和2.1中介紹的完全一樣。
  1. package com.ztesoft.provisioning.local.impl;
  2. import com.ztesoft.provisioning.local.ILocalObject;
  3. /**
  4. * 接口的實現類
  5. * @author Lethorld
  6. *
  7. */
  8. public class LocalObjectImpl implements ILocalObject {
  9. public String localCall(String arg) {
  10. // TODO : do something.
  11. return arg;
  12. }
  13. }
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.local.impl;  
  2.   
  3. import com.ztesoft.provisioning.local.ILocalObject;  
  4.   
  5. /** 
  6.  * 接口的實現類 
  7.  * @author Lethorld 
  8.  * 
  9.  */  
  10. public class LocalObjectImpl implements ILocalObject {  
  11.     public String localCall(String arg) {  
  12.         // TODO : do something.  
  13.         return arg;  
  14.     }  
  15. }  
現在我們要改一下我們的主函數,用接口的方式完成對方法的調用。下面代碼的17行,我們定義了一個接口對象object,它指向一個實現了該接口的類LocalObjectImpl的實例。
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.local;  
  2.   
  3. import com.ztesoft.provisioning.local.impl.LocalObjectImpl;  
  4.   
  5. /** 
  6.  * 本地調用者 
  7.  * @author Lethorld 
  8.  * 
  9.  */  
  10. public class LocalInvoker {  
  11.     /** 
  12.      * 主函數 
  13.      * @param args 入參 
  14.      */  
  15.     public static void main(String[] args) {  
  16.         ILocalObject object = new LocalObjectImpl();  
  17.         String retn = object.localCall("Hello");  
  18.         System.out.println(retn);  
  19.     }  
  20. }  

2.3 萬事俱備!讓我們開始向RMI進發。

有了2.1和2.2的鋪墊,現在我們可以嘗試寫一個由JRMI實現的遠程調用的程序了。
首先,我們定義一個接口,該接口要繼承自Remote接口。Remote接口在jdk中的解釋爲:“用於標識其方法可以從非本地虛擬機上調用的接口。”其實原理很簡單,我們要使用遠程調用的特性,那麼註定我們的類和簡單的本地類之間的處理是不完全一樣的。比如說:怎麼遠程通信?怎麼序列化(反序列化)入參、返回值?對於這些差異的處理,要麼我們用戶自己實現,要麼丟給RMI,由RMI框架幫我們實現。當然,作爲用戶,我們不希望每次寫個遠程調用都去處理這些共性的東西。於是,RMI框架抽象出來一個Remote接口。這樣,對於Java程序來說,凡是遇到繼承自Remote的接口,Java編譯器就知道這個類可能會被遠程調用了。
其二,所有的遠程調用的接口的方法聲明中,必須含有RemoteException的聲明。從JDK文檔中可以看出來,RemoteException是繼承自IOException。這是什麼意思呢?因爲我們遠程調用是通過網絡實現的,所以對於服務器上的異常,客戶端可以統一認爲是IO讀寫異常。這也就不難理解爲什麼RemoteException會被定義成IOException的子類了。這裏的處理個人理解可能有點泛化的意味在裏面。遠程調用,在服務器上(或者在通訊過程中)可能會發生異常的錯誤,這點所有人都可以保證。而反過來說,沒有人可以保證遠程調用,在服務器上(或者在通訊過程中)永遠不發生錯誤。既然這樣,那我們就規定:所有繼承Remote的接口的方法聲明中,必須含有RemoteException的聲明。
下面是一個繼承自Remote接口的接口,接口取名爲IRemoteTest。IRemoteTest內部有個remoteTest的方法的聲明。這個方法接受一個String類型的入參,返回值的類型也是String的。同時,它需要申明可能拋出RemoteException異常。
Java代碼  收藏代碼
  1. package com.ztesoft.provisioning.rmiserver;  
  2.   
  3. import java.rmi.Remote;  
  4. import java.rmi.RemoteException;  
  5. /** 
  6.  * 遠程調用RMI,定義的接口 
  7.  * @author Lethorld 
  8.  * Remote 接口用於標識其方法可以從非本地虛擬機上調用的接口。 
  9.  * 任何遠程對象都必須直接或間接實現此接口。只有在“遠程接口” 
  10.  *(擴展 java.rmi.Remote 的接口)中指定的這些方法纔可遠程使用。  
  11.  * 實現類可以實現任意數量的遠程接口,並且可以擴展其他遠程實現類。 
  12.  * RMI 提供一些遠程對象實現可以擴展的有用類,這些類便於遠程對象創建。 
  13.  * 這些類是 java.rmi.server.UnicastRemoteObject  
  14.  * 和 java.rmi.activation.Activatable。 
  15.  */  
  16. public interface IRemoteTest extends Remote{  
  17.     public String remoteTest(String name) throws RemoteException;  
  18. }  
接口定義好了,我們可以先把客戶端的調用程序寫完(接口存在的好處之一就是在不知道方法如何實現的前提下,我們同樣可以完成對該方法調用的後續操作)。對於2.2中的調用,我們用一句
Java代碼  收藏代碼
  1. ILocalObject object = new LocalObjectImpl();  
便可以實現創建一個LocalObjectImpl對象,並把這個對象賦給ILocalObject接口的任務。但是遠程調用不行,因爲你無法確切地知道遠程服務器上的對象的名稱(它並不是你本地的某個java類,它不受你的管束)。我們需要一個新的機制可以幫助我們獲得遠程對象的引用。這部分的工作有類Naming類實現。JDK中對該類的描述爲:“Naming 類提供在對象註冊表中存儲和獲得遠程對遠程對象引用的方法。”那麼如何去定位一個遠程對象呢?換句話說,我們如何唯一標識一個遠程對象?讓我們回到通常的WEB程序來看看究竟該如何處理。通常,我們要訪問某一個網站,我們會在瀏覽器中輸入網址。比如我們要訪問csdn網站,我們可以輸入http://www.csdn.net,點擊確定,於是我們就可以瀏覽csdn的主頁了。同樣,我們還可以輸入http://117.79.93.196來訪問csdn主頁。一般情況下,一個網站的地址會是這樣的結構:http://ip:port。http://標誌協議名,ip:port是典型的tcp地址。這樣我們便能定位到某臺機器上的某個特定的發佈了的程序了。同理在瀏覽器中輸入http://write.blog.csdn.net/postedit/7089374,你便會進入csdn博客的某篇文章(其實就是本文啦),也就是能訪問到csdn網站的某個子資源。於是,我們可以認爲,對於web應用http://ip:port/sub1/subsub1/...可以定位一個唯一的資源。對於RMI,其實貌似也是這麼做的,只不過RMI不是HTTP,所以不用HTTP://開頭;RMI貌似也不需要使用sub1/subsub1這樣繁雜的子層嵌套結構。故同理:一個rmi://ip:port/name可以唯一定位一個RMI服務器上的發佈了的對象。那麼事實是不是這樣呢?請看JDK關於Naming類的介紹,你會驚奇的發現,世界上的事,那都是想通的~
那麼,剩下的就太簡單了:
Java代碼  收藏代碼
  1. package test.com.ztesoft.zsmart.bss.provisioning.serviceActivation;  
  2.   
  3. import java.net.MalformedURLException;  
  4. import java.rmi.Naming;  
  5. import java.rmi.NotBoundException;  
  6. import java.rmi.RemoteException;  
  7. import com.ztesoft.provisioning.rmiserver.IRemoteTest;  
  8.   
  9. public class RmiSynCallTest {  
  10.     public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException {  
  11.         String url = "rmi://localhost:9527/rmitest";  
  12.         IRemoteTest test = (IRemoteTest)Naming.lookup(url);               
  13.         System.out.println(test.remoteTest("Lethorld"));  
  14.     }  
  15. }  
發佈了16 篇原創文章 · 獲贊 14 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章