1. 程式人生 > 其它 >java-RMI遠端呼叫

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獲取遠端方法的相關資訊並且呼叫

互動過程

  1. 首先,啟動RMI Registry服務,啟動時可以指定服務監聽的埠,也可以使用預設的埠(1099);
  2. 其次,Server端在本地先例項化一個提供服務的實現類,然後通過RMI提供的Naming/Context/Registry等類的bind
    rebind方法將剛才例項化好的實現類註冊到RMI Registry上並對外暴露一個名稱;
  3. 最後,Client端通過本地的介面和一個已知的名稱(即RMI Registry暴露出的名稱),使用RMI提供的Naming/Context/Registry等類的lookup方法從RMI Service那拿到實現類。這樣雖然本地沒有這個類的實現類,但所有的方法都在接口裡了,便可以實現遠端呼叫物件的方法了;

遠端物件

遠端物件是存在於服務端以供客戶端呼叫的物件。任何可以被遠端呼叫的物件都必須實現 java.rmi.Remote 介面,遠端物件的實現類必須繼承UnicastRemoteObject類。如果不繼承UnicastRemoteObject類,則需要手工初始化遠端物件,在遠端物件的構造方法的呼叫UnicastRemoteObject.exportObject()

靜態方法。這個遠端物件中可能有很多個函式,但是隻有在遠端介面中宣告的函式才能被遠端呼叫,其他的公共函式只能在本地的JVM中使用。

使用遠端方法呼叫,必然會涉及引數的傳遞和執行結果的返回。引數或者返回值可以是基本資料型別,當然也有可能是物件的引用。所以這些需要被傳輸的物件必須可以被序列化,這要求相應的類必須實現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):登出物件,取消物件與名字的繫結;