RMI遠程調用基本概念
-
Java的RMI遠程調用是指,一個JVM中的代碼可以通過
網絡
實現遠程調用
另一個JVM的某個方法。RMI是Remote Method Invocation的縮寫。 -
提供服務
的一方我們稱之爲服務端
,而實現遠程調用
的一方我們稱之爲客戶端
。
Java實現RMI遠程調用
實現一個最簡單的RMI:服務器會提供一個WorldClock服務,允許客戶端獲取指定時區的時間,即允許客戶端調用下面的方法:
LocalDateTime getLocalDateTime(String zoneId);
-
要實現RMI,
服務器和客戶端
必須共同實現同一個接口。且該接口必須繼承java.rmi.Remote
,並在每個方法聲明拋出RemoteException
要求:
- 服務端與客戶端必須同時共享繼承java.rmi.Remote的接口
- 共享接口必須繼承java.rmi.Remote
- 共享接口所有方法必須聲明拋出java.rmi.RemoteException 異常
我們定義一個WorldClock接口,代碼如下:
public interface WorldClock extends Remote { LocalDateTime getLocalDateTime(String zoneId) throws RemoteException; }
Java的RMI規定此接口必須
繼承java.rmi.Remote
,並在每個方法聲明拋出RemoteException
-
編寫服務端的實現類,並將服務通過RMI暴露到網絡(
註冊遠程服務
)上,因爲客戶端請求的調用方法getLocalDateTime()最終會通過這個實現類返回結果。實現類
WorldClockService
代碼如下:public class WorldClockService implements WorldClock { @Override public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException { return LocalDateTime.now(ZoneId.of(zoneId)).withNano(0); } }
服務端的服務相關代碼就編寫完畢後,我們需要通過Java RMI提供的一系列
底層支持接口
,把上面編寫的服務以RMI的形式暴露在網絡上
,客戶端才能調用:public class Server { public static void main(String[] args) throws RemoteException { System.out.println("create World clock remote service..."); // 實例化一個WorldClock: WorldClock worldClock = new WorldClockService(); // 將此服務轉換爲遠程服務接口: WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0); // 將RMI服務註冊到1099端口: Registry registry = LocateRegistry.createRegistry(1099); // 註冊此服務,服務名爲"WorldClock": registry.rebind("WorldClock", skeleton); } }
上面代碼是通過RMI提供的相關類,將我們自己的
WorldClock實例
註冊到RMI服務上。RMI的默認端口是1099,最後一步註冊服務時通過 rebind() 指定服務名稱爲"WorldClock"
。 -
編寫客戶端代碼(
RMI要求服務器和客戶端共享同一個接口
)。 即在客戶端必須能夠引用共享接口public class Client { public static void main(String[] args) throws RemoteException, NotBoundException { // 連接到服務器localhost,端口1099: Registry registry = LocateRegistry.getRegistry("localhost", 1099); // 查找名稱爲"WorldClock"的服務並強制轉型爲WorldClock接口: WorldClock worldClock = (WorldClock) registry.lookup("WorldClock"); // 正常調用接口方法: LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai"); // 打印調用結果: System.out.println(now); } }
-
先運行服務端,再運行客戶端。客戶端只有接口沒有實現類,因此客戶端獲得的接口方法返回值實際上是通過
網絡
從服務器端獲取的。- 整個過程實際上非常簡單,對客戶端來說,客戶端持有的WorldClock接口實際上對應了一個“實現類”,它是由
Registry
內部動態生成的,並負責把方法調用
通過網絡
傳遞到服務端
。 - 而服務端接收網絡調用的服務並不是
我們自己編寫
的WorldClockService,而是Registry自動生成的代碼
。 - 我們把客戶端的
“實現類”
稱爲stub
,而服務器端的網絡服務類
稱爲skeleton
,它會真正調用服務端
的WorldClockService,獲取結果,然後把結果通過網絡傳遞給客戶端
。 - 整個過程由
RMI底層
負責實現序列化和反序列化
:
- 整個過程實際上非常簡單,對客戶端來說,客戶端持有的WorldClock接口實際上對應了一個“實現類”,它是由
Java實現RMI遠程調用2
- 編寫服務端程序
- 第一步: 創建遠程接口
public interface WorldClock extends Remote {
LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}
要求:
- 共享接口必須繼承java.rmi.Remote
- 所有方法必須拋出java.rmi.RemoteException 異常
- 第二步: 創建實現類
public class WorldClockService extends UnicastRemoteObject implements WorldClock {
private static final long serialVersionUID = 1668947611852931187L;
protected WorldClockService() throws RemoteException { }
@Override
public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException {
return LocalDateTime.now(ZoneId.of(zoneId)).withNano(0);
}
}
要求:
- 實現類必須繼承java.rmi.server.UnicastRemoteObject
- 實現類必須聲明一個無參受保護的構造方法且方法聲明拋出RemoteException
- 第三步: 註冊遠程服務,將服務通過uri 暴露給其它人使用
public class Server2 {
public static void main(String[] args) throws RemoteException, MalformedURLException {
//註冊通訊端口
LocateRegistry.createRegistry(1099);
//註冊通訊路徑
Naming.rebind("rmi://192.168.0.101:1099/WorldClock", new WorldClockService());
System.out.println("啓動服務器");
}
}
注意:
- 可以註冊多個服務 (可以對外暴露多個服務)
- URL的命名規則需要遵循,也就是和上面格式一模一樣
- 編寫客戶端程序
-
第一步: 將接口複製到客戶端
客戶端必須也擁有,一個和服務端一樣的接口, 並且這個接口所在的路徑 ,必須和服務端的接口一模一樣
服務端 -
第二步: 使用服務器端暴露的接口獲取數據
public class Client2 {
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
//通過命名空間找到 通訊服務
WorldClock worldClock = (WorldClock) Naming.lookup("rmi://192.168.0.101:1099/WorldClock");
// 正常調用接口方法:
LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai");
// 打印調用結果:
System.out.println(now);
}
}
執行結果:
Java的RMI遠程調用弊端
- Java的RMI嚴重依賴
序列化和反序列化
這可能會造成嚴重的安全漏洞 - 因爲Java的序列化和反序列化
不但涉及到數據,還涉及到二進制的字節碼
,即使使用白名單機制
也很難保證100%排除惡意構造的字節碼。 - 因此使用RMI時,雙方必須是內網互相信任的機器,服務端不要把端口暴露在公網上作爲對外服務。
- 此外,Java的RMI調用機制決定了
雙方必須是Java程序
,其他語言很難調用Java的RMI。 如果要使用不同語言進行RPC調用,可以選擇更通用的協議,例如gRPC(grpc是谷歌的一個開源的rpc(遠程服務調用)框架(
。
- grpc是谷歌的一個開源的rpc(遠程服務調用)框架,可以讓各個語言按照指定的規則通過http2協議相互調用,這個規則是用Protocol Buffer(谷歌的一個數據描述語言)寫的一個.proto文件,grpc的目的就是爲了讓服務調用更方便。
- 目前支持的語言有C, C++,C#,Java, Node.js, Python,Go等,大部分語言都是通過插件根據.proto文件生成對應的代碼,用生成好的代碼,創建或調用grpc服務。
- gRPC和restful API都提供了一套通信機制,用於server/client模型通信,而且它們都使用http作爲底層的傳輸協議(嚴格地說, gRPC使用的http2.0,而restful api則不一定)
小結
-
Java提供了RMI實現遠程方法調用:
-
RMI通過自動生成stub和skeleton實現網絡調用,客戶端只需要查找服務並獲得接口實例,服務器端只需要編寫實現類並註冊爲服務;
-
RMI的序列化和反序列化可能會造成安全漏洞,因此調用雙方必須是內網互相信任的機器,服務端不要把端口暴露在公網上作爲對外服務。