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流程可以大概描述為:

  1. Registry先啟動,並監聽一個埠,一般為1099
  2. Server向Registry註冊遠程對象
  3. Client從Registry獲得遠程對象的代理(這個代理知道遠程對象的在網路中的具體位置:ip、埠、標識符),然後Client通過這個代理調用遠程方法,Server也是有一個代理的,Server端的代理會收到Client端的調用的方法、參數等,然後代理執行對應方法,並將結果通過網路返回給Client。

兩圖勝千言:

其實跟上圖都類似

0x02、RMI的框架與解析

RMI調用遠程方法的大致如下:

  1. RMI客戶端在調用遠程方法時會先創建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub會將Remote對象傳遞給遠程引用層(java.rmi.server.RemoteRef)並創建java.rmi.server.RemoteCall(遠程調用)對象。
  3. RemoteCall序列化RMI服務名稱Remote對象。
  4. RMI客戶端遠程引用層傳輸RemoteCall序列化後的請求資訊通過Socket連接的方式傳輸到RMI服務端遠程引用層
  5. RMI服務端遠程引用層(sun.rmi.server.UnicastServerRef)收到請求會請求傳遞給Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton調用RemoteCall反序列化RMI客戶端傳過來的序列化。
  7. Skeleton處理客戶端請求:bindlistlookuprebindunbind,如果是lookup則查找RMI服務名綁定的介面對象,序列化該對象並通過RemoteCall傳輸到客戶端。
  8. RMI客戶端反序列化服務端結果,獲取遠程對象的引用。

而更通俗點來說:

  1. 客戶端請求代理
  2. Stub編碼處理消息
  3. 消息傳輸
  4. 到達管家skeleton並處理資訊
  5. 管家skeleton把資訊提交給server
  6. server接收到請求
  7. server把請求的結果給管家
  8. 管家skeleton把結果轉交給stub
  9. 代理Stub對結果解碼
  10. 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

    }
}

網上參考學習,如若有錯,請各位大佬指出