java-RMI遠端呼叫
什麼是RMI
Java RMI是專為Java環境設計的遠端方法呼叫機制,是一種用於實現遠端呼叫(RPC,Remote Procedure Call)的Java API,能直接傳輸序列化後的Java物件和分散式垃圾收集。它的實現依賴於JVM,因此它支援從一個JVM到另一個JVM的呼叫。
在Java RMI中,遠端伺服器實現具體的Java方法並提供介面,客戶端本地僅需根據介面類的定義,提供相應的引數即可呼叫遠端方法,其中物件是通過序列化方式進行編碼傳輸的。所以平時說的反序列化漏洞的利用經常是涉及到RMI,就是這個意思。
RMI依賴的通訊協議為JRMP(Java Remote Message Protocol,Java遠端訊息交換協議),該協議是為Java定製的,要求服務端與客戶端都必須是Java編寫的。
RMI的模式與互動過程
設計模式
RMI的設計模式中,主要包括以下三個部分的角色:
- Registry:提供服務註冊與服務獲取。即Server端向Registry註冊服務,比如地址、埠等一些資訊,Client端從Registry獲取遠端物件的一些資訊,如地址、埠等,然後進行遠端呼叫。
- Server:遠端方法的提供者,並向Registry註冊自身提供的服務
- Client:遠端方法的消費者,從Registry獲取遠端方法的相關資訊並且呼叫
互動過程
- 首先,啟動RMI Registry服務,啟動時可以指定服務監聽的埠,也可以使用預設的埠(1099);
- 其次,Server端在本地先例項化一個提供服務的實現類,然後通過RMI提供的Naming/Context/Registry等類的
bind
rebind
方法將剛才例項化好的實現類註冊到RMI Registry上並對外暴露一個名稱; - 最後,Client端通過本地的介面和一個已知的名稱(即RMI Registry暴露出的名稱),使用RMI提供的Naming/Context/Registry等類的
lookup
方法從RMI Service那拿到實現類。這樣雖然本地沒有這個類的實現類,但所有的方法都在接口裡了,便可以實現遠端呼叫物件的方法了;
遠端物件
遠端物件是存在於服務端以供客戶端呼叫的物件。任何可以被遠端呼叫的物件都必須實現 java.rmi.Remote
介面,遠端物件的實現類必須繼承UnicastRemoteObject
類。如果不繼承UnicastRemoteObject類,則需要手工初始化遠端物件,在遠端物件的構造方法的呼叫UnicastRemoteObject.exportObject()
使用遠端方法呼叫,必然會涉及引數的傳遞和執行結果的返回。引數或者返回值可以是基本資料型別,當然也有可能是物件的引用。所以這些需要被傳輸的物件必須可以被序列化,這要求相應的類必須實現java.io.Serializable
介面,並且客戶端的serialVersionUID
欄位要與伺服器端保持一致。
流程原理
從RMI設計角度來講,基本分為三層架構模式來實現RMI,分別為RMI服務端,RMI客戶端和RMI註冊中心。
客戶端
存根/樁(Stub):遠端物件在客戶端上的代理;
遠端引用層(Remote Reference Layer):解析並執行遠端引用協議;
傳輸層(Transport):傳送呼叫、傳遞遠端方法引數、接收遠端方法執行結果
服務端
骨架(Skeleton):讀取客戶端傳遞的方法引數,呼叫伺服器方的實際物件方法, 並接收方法執行後的返回值;
遠端引用層(Remote Reference Layer):處理遠端引用後向骨架傳送遠端方法呼叫;
傳輸層(Transport):監聽客戶端的入站連線,接收並轉發呼叫到遠端引用層。
登錄檔(Registry)
以URL形式註冊遠端物件,並向客戶端回覆對遠端物件的引用。
流程原理圖
編寫RMI步驟
定義服務端供遠端呼叫的類
在此之前先定義一個可序列化的Model層的使用者類,其例項可放置於服務端進行遠端呼叫:
import java.io.Serializable;
public class PersonEntity implements Serializable {
private int id;
private String name;
private int age;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
定義一個遠端介面
遠端介面必須繼承java.rmi.Remote介面,且丟擲RemoteException錯誤:
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface PersonService extends Remote {
public List<PersonEntity> GetList() throws RemoteException;
}
開發介面的實現類
建立PersonServiceImpl實現遠端介面,注意此為遠端物件實現類,需要繼承UnicastRemoteObject(如果不繼承UnicastRemoteObject類,則需要手工初始化遠端物件,在遠端物件的構造方法的呼叫UnicastRemoteObject.exportObject()靜態方法):
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.List;
public class PersonServiceImpl extends UnicastRemoteObject implements PersonService {
public PersonServiceImpl() throws RemoteException {
super();
// TODO Auto-generated constructor stub
}
@Override
public List<PersonEntity> GetList() throws RemoteException {
// TODO Auto-generated method stub
System.out.println("Get Person Start!");
List<PersonEntity> personList = new LinkedList<PersonEntity>();
PersonEntity person1 = new PersonEntity();
person1.setAge(3);
person1.setId(0);
person1.setName("mi1k7ea");
personList.add(person1);
PersonEntity person2 = new PersonEntity();
person2.setAge(18);
person2.setId(1);
person2.setName("Alan");
personList.add(person2);
return personList;
}
}
建立Server和Registry
其實Server和Registry可以單獨執行建立,其中Registry可通過程式碼啟動也可通過rmiregistry命令啟動,這裡只進行簡單的演示,將Server和Registry的建立、物件繫結登錄檔等都寫到一塊,且Registry直接程式碼啟動:
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class Program {
public static void main(String[] args) {
try {
PersonService personService=new PersonServiceImpl();
//註冊通訊埠
LocateRegistry.createRegistry(6600);
//註冊通訊路徑
Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
System.out.println("Service Start!");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
建立客戶端並查詢呼叫遠端方法
這裡我們通過Naming.lookup()來查詢RMI Server端的遠端物件並獲取到本地客戶端環境中輸出出來:
import java.rmi.Naming;
import java.util.List;
public class Client {
public static void main(String[] args){
try{
//呼叫遠端物件,注意RMI路徑與介面必須與伺服器配置一致
PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
List<PersonEntity> personList=personService.GetList();
for(PersonEntity person:personList){
System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
}
}catch(Exception ex){
ex.printStackTrace();
}
}
}
函式說明
- bind(String name, Object obj):註冊物件,把物件和一個名字name繫結,這裡的name其實就是URL格式。如果改名字已經與其他物件繫結,則丟擲NameAlreadyBoundException錯誤;
- rebind(String name, Object obj):註冊物件,把物件和一個名字name繫結。如果改名字已經與其他物件繫結,不會丟擲NameAlreadyBoundException錯誤,而是把當前引數obj指定的物件覆蓋原先的物件;
- lookup(String name):查詢物件,返回與引數name指定的名字所繫結的物件;
- unbind(String name):登出物件,取消物件與名字的繫結;