Java 中最簡單的分布式調用 RMI
JAVA中最簡單的分布式調用 RMI
前言
我們先來看一個例子:
系統中目前存在兩個 JAVA 服務,分別是服務A、服務B。現在服務A 想要調用服務B中的某個服務,我們怎么實現呢?
有人覺得這不很簡單,服務B暴露一個服務接口,服務A通過 RPC 的方式來訪問這個接口,這里的 RPC 可以引用第三方實現,也可以通過簡單的 REST 請求的方式實現。
是的,解決這場景的方法有很多,其實 JAVA 自身也提供了一種更簡單的方式,即通過 RMI 實現跨 JVM 虛擬機的遠程調用。雖然它和現在主流的 RPC 相比,可能顯得比較無力。但是其設計思想,加上它的簡單易用,我們不妨來看一下。
RMI 簡介
RMI(Remote Method Invocation)是一種用于實現遠程過程調用的應用程序編程接口。它使客戶機上運行的程序可以調用遠程服務器上的對象。遠程方法調用特性使Java編程人員能夠在網絡環境中分布操作。
特點
- 是 JAVA 自帶的功能,無需集成任何的外部擴展;
- 數據傳輸是面向對象的;
- 動態下載對象資源;
- 僅限 JAVA 間通信;
通信協議
服務間的通信通過 TCP 傳輸。協議約定為 rmi://,僅限JAVA之間的遠程通信;
成員
- RMI Registry:作為存儲遠程服務的代理對象的倉庫
- Server:服務端,暴露遠程對象,并將其代理對象注冊進 RMI Registry
- Client:客戶端,查找遠程代理對象,遠程調用服務對象
運行機制
從上圖可以看出,雖然 RMI 目前看上去有點過時了,但其思想和現在的服務注冊與發現還是很相似的。歸納起來,包含以下幾點:
- 啟動注冊中心
- 服務端:暴露服務
- 服務端:服務注冊
- 客戶端:獲取服務地址(代理對象)
- 客戶端:遠程調用服務
使用方法
- 啟動 RMI Registry
這里啟動倉庫有兩種方式,一種是在程序中啟動:
import java.rmi.registry.LocateRegistry;
Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);
另一種通過命令啟動:
/usr/bin/rmiregistry REGISTRY_PORT
- 獲取 RMI Registry
- 通過環境變量 java.rmi.server.hostname 來設置倉庫地址
import java.rmi.registry.LocateRegistry; Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT)
-
定義遠程服務接口
- 接口繼承 Remote
- 接口方法必須拋出 RemoteException
import java.rmi.Remote; public interface RemoteService extends Remote {
//define your function Object run() throws RemoteException;
}</code></pre>
-
UnicastRemoteObject.exportObject(Remote obj, int port)
- 創建 Remote 對象的代理類,并實現 Serializable 接口
- 在 TCP 上暴露遠程服務
- port 為 0 表示使用匿名隨機端口 ( 使用1~1023的已知端口時,注意權限問題 )
import java.rmi.server.UnicastRemoteObject; Remote remoteProxy = UnicastRemoteObject.exportObject(your_remote_service, 0);
- 注冊遠程對象到 RMI Registry( 在 Registry 中的都是對象的遠程代理類,并非真正的對象 )
獲取 Registry 的遠程代理類,然后調用它的 rebind 將代理對象注冊進倉庫中 ( Naming.rebind(String name, Remote obj) 本質上也是解析 name 中的倉庫地址,獲取倉庫的代理對象,進而進行遠程注冊 )
// 本地創建或遠程獲取 Registry Registry registry = ... registry.rebind(String name, Remote obj);
- 查找遠程調用對象
Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT); Remote obj = registry.lookup(REMOTE_NAME);
示例
###準備工作: 定義遠程對象接口
package com.test.remote;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface RemoteService extends Remote {
Object run() throws RemoteException; Object run(Object obj) throws RemoteException;
}</code></pre>
###服務B:注冊遠程服務
- 實現遠程服務對象
package com.test.serviceB.publishService;
import com.test.remote.RemoteService; import java.rmi.RemoteException;
public class pService1 implements RemoteService {
public Object run() { System.out.println("invoke pService1."); return "success"; } public Object run(Object obj) throws RemoteException { System.out.println("invoke pService1, params is " + obj.toString()); return "success"; }
}</code></pre>
-
啟動服務
- 創建 RMI Registry(也可在通過命令 rmiregistry 在應用外創建)
- 實例化遠程服務
- 導出遠程對象,使其能接受遠程調用
- 將導出的遠程對象綁定到倉庫中
- 等待服務調用
public class Boot {
private static final String REMOTE_P1 = "serviceB:p1"; private static final int REGISTRY_PORT = 9999; public static void main(String[] args) throws RemoteException { // 實例化遠程對象,并創建遠程代理類 RemoteService p1 = new pService1(); Remote stub1 = UnicastRemoteObject.exportObject(p1, 0); // 本地創建 Registry,并注冊遠程代理類 Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT); registry.rebind(REMOTE_P1, stub1); System.out.println("service b bound"); }
}</code></pre>
###服務A:調用遠程服務
- 啟動服務
- 連接倉庫
- 在 Registry 中查找所調用服務的遠程代理類
- 調用代理類方法
public class Boot {
private static final String REMOTE_P1 = "serviceB:p1"; private static final int REGISTRY_PORT = 9999; public static void main(String[] args) throws RemoteException { try { Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT); // 從倉庫中獲取遠程代理類 RemoteService p1 = (RemoteService) registry.lookup(REMOTE_P1); // 遠程動態代理 String res1 = (String)p1.run(); System.out.printf("The remote call for %s %s \n", REMOTE_P1, res1); } catch (NotBoundException e){ e.printStackTrace(); } catch (RemoteException e){ e.printStackTrace(); } }
}</code></pre>
演示結果
- 啟動服務B
service b bound
- 啟動服務A
The remote call for serviceB:p1 success
Process finished with exit code 0</code></pre>
- 查看服務B 調用情況
service b bound invoke pService1.
高級用法
上面示例沒有涉及到遠程調用的傳參問題。如果需要傳參,且傳參的類型不是基本類型時,遠程服務就需要動態的去下載資源。
這里通過設置環境變量來實現遠程下載:
- java.rmi.server.codebase:遠程資源下載路徑(必須是絕對路徑),可以是file://, ftp://, http:// 等形式的;
- java.rmi.server.useCodebaseOnly:默認為 true, 表示僅依賴當前的 codebase, 如果使用外部的 codebase( 服務B 需要使用 服務A 提供的下載地址時 ),需將此參數設置為false;
對于跨主機的訪問,RMI 加入了安全管理器(SecurityManager),那么也需要對應的安全策略文件
- java.security.policy:指定策略文件地址;
其他設置:
- java.rmi.server.hostname:設置倉庫的主機地址;
- sun.rmi.transport.tcp.handshakeTimeout:設置連接倉庫的超時時間;
核心代碼
關于源碼的閱讀,網上曾經看到一句話講的很好,“源碼閱讀的什么樣程度算好,閱讀到自己能放過自己了,那就夠了。”
我一般喜歡帶著問題來閱讀,這里我從幾個問題入手,簡單分享下我的理解。
- 獲取到的 Registry 對象到底是什么東西?
從這段代碼來分析:
Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);
- 遠程對象到底是什么,原始對象又在哪里呢?
從這段代碼來分析:
Remote obj = UnicastRemoteObject.exportObject(Remote obj, int port);
- 知道了倉庫中存放、獲取的都是 遠程對象的代理類,那么實際的遠程通信是如何完成的?
知道 JDK 動態代理的同學,肯定有一個 invoke 方法,是方法調用的關鍵。這里的 invoke() 具體代碼在 UnicastRef 中。
問題
從源碼中可以看出,遠程調用每次都會 新建一個 connection,感覺這里會成為一個性能的瓶頸。
總結
雖然 RMI 在目前看來有些過時了,但它的思想:遠程倉庫、服務注冊、服務查找、代理調用等,和目前主流的 RPC 是不是很相似呢?一種技術的過時,往往是跟不上業務的快速發展,但它的產生至少是滿足了當時的需求。
個人覺得,技術的實現會隨著業務的發展不斷的變化,但是核心思想一定是小步的進行,畢竟這些都是不斷積累的經驗總結出來的。希望本篇對大家能有所收獲!
來自:https://github.com/jasonGeng88/blog/blob/master/201704/rmi.md