RMI之由淺入深(一)
0x01、什麼是RMI
RMI(Remote Method Invocation)即Java遠程方法調用,RMI用於構建分散式應用程式,RMI實現了Java程式之間跨JVM的遠程通訊。顧名思義,遠程方法調用:客戶端比如說是在手機,然後服務端是在電腦;同時都有java環境,然後我要在手機端調用服務端那邊的某個方法,這就是,遠程方法調用;
使用RMI的時候,客戶端對遠程方法的調用就跟對同一個Java虛擬機(也就是本地)上的方法調用是一樣的。一般調用和RMI調用有一點不同,雖然對客戶端來說看起來像是本地的,但是客戶端的stub會通過網路發出調用,所以會拋出異常;其中還是會涉及到Socket和串流的問題,一開始是本地調用,然後就代理(stub)會轉成遠程,中間的資訊是如何從Java虛擬機發送到另外一台Java虛擬機要看客戶端和服務端的輔助設施對象所用的協議而定;使用RMI的時候,需要選擇協議:JRMP或IIOP協議;JRMP是RMI的原生的協議,也就是默認JRMP協議。而IIOP是為了CORBA而產生的~~~
遠程方法調用,具體怎麼實現呢?遠程伺服器提供具體的類和方法,本地會通過某種方式獲得遠程類的一個代理,然後通過這個代理調用遠程對象的方法,方法的參數是通過序列化與反序列化的方式傳遞的,所以,
1. 只要服務端的對象提供了一個方法,這個方法接收的是一個Object類型的參數
2. 且遠程伺服器的classpath中存在可利用pop鏈,那麼我們就可以通過在客戶端調用這個方法,並傳遞一個精心構造的對象的方式來攻擊rmi服務。
某種方式獲得遠程對象的代理,那麼具體是怎麼的實現機制呢?RMI模式中除了有Client與Server,還藉助了一個Registry(註冊中心)。
Server | Registry | Client |
---|---|---|
提供具體的遠程對象 | 一個註冊表,存放著遠程對象的位置(ip、埠、標識符) | 遠程對象的使用者 |
其中Server與Registry可以在同一伺服器上實現,也可以布置在不同伺服器上,現在一個完整的RMI流程可以大概描述為:
- Registry先啟動,並監聽一個埠,一般為1099
- Server向Registry註冊遠程對象
- Client從Registry獲得遠程對象的代理(這個代理知道遠程對象的在網路中的具體位置:ip、埠、標識符),然後Client通過這個代理調用遠程方法,Server也是有一個代理的,Server端的代理會收到Client端的調用的方法、參數等,然後代理執行對應方法,並將結果通過網路返回給Client。
兩圖勝千言:
其實跟上圖都類似
0x02、RMI的框架與解析
RMI調用遠程方法的大致如下:
RMI客戶端
在調用遠程方法時會先創建Stub(sun.rmi.registry.RegistryImpl_Stub)
。Stub
會將Remote
對象傳遞給遠程引用層(java.rmi.server.RemoteRef)
並創建java.rmi.server.RemoteCall(遠程調用)
對象。RemoteCall
序列化RMI服務名稱
、Remote
對象。RMI客戶端
的遠程引用層
傳輸RemoteCall
序列化後的請求資訊通過Socket
連接的方式傳輸到RMI服務端
的遠程引用層
。RMI服務端
的遠程引用層(sun.rmi.server.UnicastServerRef)
收到請求會請求傳遞給Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
。Skeleton
調用RemoteCall
反序列化RMI客戶端
傳過來的序列化。Skeleton
處理客戶端請求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
則查找RMI服務名
綁定的介面對象,序列化該對象並通過RemoteCall
傳輸到客戶端。RMI客戶端
反序列化服務端結果,獲取遠程對象的引用。
而更通俗點來說:
- 客戶端請求代理
- Stub編碼處理消息
- 消息傳輸
- 到達管家skeleton並處理資訊
- 管家skeleton把資訊提交給server
- server接收到請求
- server把請求的結果給管家
- 管家skeleton把結果轉交給stub
- 代理Stub對結果解碼
- Stub把解碼的結果交給client。
更容易的通俗易懂~~~
1、⾸先客戶端連接Registry,並在其中尋找Name是Hello的對象
2、這個對應數據流中的Call消息;
3、然後Registry返回⼀個序列化的數據,這個就是找到的Name=Hello的對象,這個對應數據流中的ReturnData消息;
4、客戶端反序列化該對象,發現該對象是⼀個遠程對象,地址在 169.254.20.76:39098 ,這邊可能會有疑問,這裡面沒有,怎麼會知道埠號啥的
可以看出,其實埠是跟在遠程地址的後面,只不過是16進位的,需要切換一下
5、於是再與這個地址建⽴TCP連接;在這個新的連接中,才執⾏真正遠程⽅法調⽤,也就是 hello()
0x03、rmi測試程式碼
RMIDemo.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface rmiDemo extends Remote {
public String hello() throws RemoteException;
}
RMIDemoImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMIDemoImpl extends UnicastRemoteObject implements rmiDemo{
protected RMIDemoImpl() throws RemoteException {
System.out.println("構造方法");
}
public String hello() throws RemoteException {
System.out.println("hello方法被調用");
return "hello,world";
}
}
RMIServer.java
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException {
rmiDemo hello = new RemoteHelloWorld();//創建遠程對象
Registry registry = LocateRegistry.createRegistry(1099);//創建註冊表
registry.rebind("hello",hello);//將遠程對象註冊到註冊表裡面,並且設置值為hello
}
}
RMIClient.java
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException {
rmiDemo hello = new RemoteHelloWorld();//創建遠程對象
Registry registry = LocateRegistry.createRegistry(1099);//創建註冊表
registry.rebind("hello",hello);//將遠程對象註冊到註冊表裡面,並且設置值為hello
}
}