前言
對RMI接觸的也比較早,基本上剛學完Java基礎不久就機緣巧合遇到了。當時有嘗試著去了解,但是沒看到比較好的教程,而且對網路程式設計相關知識不太瞭解,看了不少文章,也沒弄明白。現在對網路和I/O有了一定了解,理解起來也比較順暢了。以下,是我對RMI的認識。
RMI的相關概念
"In computing, the Java Remote Method Invocation (Java RMI) is a Java API that performs remote method invocation, the object-oriented equivalent of remote procedure calls (RPC), with support for direct transfer of serialized Java classes and distributed garbage-collection." ——維基百科
上述定義指明幾個點
- RMI是Java用於呼叫遠端方法的API ——①屬於Java ②指的是一個API,不是協議或者其他什麼東西
- 相當於物件導向版的RPC —— RPC是遠端過程呼叫(程式導向)
- 支援直接傳輸可序列化的Java類 ——①傳輸的類是方法呼叫需要的引數和返回值,而不是實現類,實現類的方法在伺服器執行。
- 支援分散式垃圾回收(暫未涉獵)
遠端呼叫
遠端呼叫並不是載入遠端的類到本地執行,而是通過傳遞方法引數到伺服器,讓伺服器執行相應的方法並返回結果給客戶端。
網路協議與IO
RMI所指的遠端呼叫,說的其實是從一個JVM的物件呼叫另一個JVM環境下物件方法的過程。兩個JVM可以在不同的主機上,也可以在同一臺主機上。如果在不同主機上,則方法呼叫必然需要網路傳輸,網路傳輸則必然與Socket通訊以及相關的通訊協議掛鉤,RMI用的底層協議是JRMIP。值得一提的是,使用RMI這個API的好處就是,使用者不需要實現底層的網路通訊與IO,或者說網路傳輸對使用者來說是透明的。
遠端介面
客戶端和服務端都有的介面,只不過介面的實現類在服務端。
RMI的工作原理
值得一提的是,我在學習RMI的時候,故我根據理解畫了這張圖。書上講到需要用rmic指令生成stub和skeleton。但是實際上,Java後面已經廢除了這種方式,skeleton被認為不再需要,但是伺服器仍然有一些東西負責skeleton的行為(誰負責,暫不清楚,尚未了解底層實現)。而stub可以動態生成。所以用這張圖理解RMI還是沒問題的。
RMI遠端服務的實現
相關概念
Remote
java.rmi.server包下的定義的介面,遠端介面必須繼承與RMI的Remote介面,表明其遠端介面的身份。
RMI registry
RMI的登錄檔服務,伺服器需要將對應的服務註冊到登錄檔上,此操操作將會繫結對應服務的stub(存根)。客戶端會根據伺服器的IP地址找到伺服器,然後在伺服器的登錄檔上找對應的存根。
UnicastRemoteObject
遠端介面的實現類必須繼承此類,以用於生成與底層JRMP協議相關的通訊物件,以及用於與通訊物件連線的stub(存根)。
服務端實現步驟
1. 定義遠端介面
定義一個介面,繼承於Remote介面。注意,定義的方法需要丟擲RemoteException。因為網路和I/O是不安全的,必須讓客戶意識到這點,做相應的異常處理準備。
2. 定義遠端介面實現
定義一個類實現上述介面,並繼承UnicastRemoteObject。可在此類,或其他類中的main方法中,註冊服務到登錄檔表。
Naming.rebind("serviceName", service);
3. 啟動RMI registry登錄檔服務
利用命令列啟動。啟動命令所在的目錄必須能夠訪問到你的類。最好在實現類目錄啟動。
%rmiregistry
4. 開啟遠端服務(註冊服務到登錄檔)
執行,帶有註冊操作的類。
客戶端實現步驟
1. 定義遠端介面
與服務端的介面相同,直接拷貝過來即可。
2. 定義訪問遠端介面的類
客戶端需要定義一個類,用於連線到服務端的登錄檔,根據服務的相關資訊查詢並獲取stub(存根)。
Naming.lookup("rmi://ip:port/serviceName");
3. 根據stub訪問遠端介面方法。
RMI樣例程式碼
檔案圖
注:在單機環境下模擬,客戶端服務端在不同的資料夾。
服務端
MyRemote.java
import java.rmi.*; public interface MyRemote extends Remote { public String sayHello() throws RemoteException; }
MyRemoteImpl.java
import java.rmi.*; import java.rmi.server.*; public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote { //實現遠端方法 public String sayHello() { return "Server says, 'Hey'"; } public MyRemoteImpl() throws RemoteException { } //主方法,註冊服務到登錄檔 public static void main(String[] args) { try { //建立遠端物件 MyRemote service = new MyRemoteImpl(); //繫結到登錄檔 //rebind方法會覆蓋同名方法,相當於先unbind再bind. Naming.rebind("RemoteHello", service); } catch(Exception e) { e.printStackTrace(); } } }
啟動RMI Registry登錄檔服務,注意,此命令沒有任何響應,就是開啟成功了,不要關閉此彈窗
啟動服務,開啟另一個控制檯,執行主程式。此主程式同樣沒有任何輸出。
客戶端
MyRemote同上
MyRemoteClient.java
import java.rmi.*; public class MyRemoteClient { public static void main(String[] args) { new MyRemoteClient().go(); } public void go() { try { //查詢對應服務,並獲取到存根 MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello"); //利用存根呼叫介面方法 String s = service.sayHello(); //輸出返回值 System.out.println(s); } catch(Exception e) { e.printStackTrace(); } } }
啟動客戶端,開啟另一個控制檯,啟動客戶端主程式
為什麼遠端介面可直接呼叫Stub
遠端介面能直接呼叫stub,那很明顯,stub就是遠端介面的實現類。前面我們提到了,用rmic命令工具生生成stub的方式已經被廢除了。stub可以動態生成。但是使用rmic命令工具還是可以生成_stub的class檔案。雖然有相關的警告。
我們可以用反編譯工具,來檢視其原始碼。我使用的是jd-gui。
即,stub確實就是遠端介面的實現類。注意,只是語法上實現,真正的實現在服務端。stub算是一個代理,讓客戶端可以像操作實現類那樣操作stub,好像實現類就在本地一樣。