【Java基礎】網絡編程-RMI遠程調用

RMI遠程調用基本概念

  • Java的RMI遠程調用是指,一個JVM中的代碼可以通過網絡實現遠程調用另一個JVM的某個方法RMI是Remote Method Invocation的縮寫。

  • 提供服務的一方我們稱之爲服務端,而實現遠程調用的一方我們稱之爲客戶端

Java實現RMI遠程調用

實現一個最簡單的RMI:服務器會提供一個WorldClock服務,允許客戶端獲取指定時區的時間,即允許客戶端調用下面的方法:

	LocalDateTime getLocalDateTime(String zoneId);
  1. 要實現RMI,服務器和客戶端必須共同實現同一個接口。且該接口必須繼承java.rmi.Remote,並在每個方法聲明拋出RemoteException

    要求:

    1. 服務端與客戶端必須同時共享繼承java.rmi.Remote的接口
    2. 共享接口必須繼承java.rmi.Remote
    3. 共享接口所有方法必須聲明拋出java.rmi.RemoteException 異常

    我們定義一個WorldClock接口,代碼如下:

    public interface WorldClock extends Remote {
    	LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
    }
    

    Java的RMI規定此接口必須繼承java.rmi.Remote,並在每個方法聲明拋出RemoteException

  2. 編寫服務端的實現類,並將服務通過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"

  3. 編寫客戶端代碼(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);
    	}
    }
    
  4. 先運行服務端,再運行客戶端。客戶端只有接口沒有實現類,因此客戶端獲得的接口方法返回值實際上是通過網絡從服務器端獲取的。

    • 整個過程實際上非常簡單,對客戶端來說,客戶端持有的WorldClock接口實際上對應了一個“實現類”,它是Registry內部動態生成的,並負責把方法調用通過網絡傳遞到服務端
    • 而服務端接收網絡調用的服務並不是我們自己編寫的WorldClockService而是Registry自動生成的代碼
    • 我們把客戶端的“實現類”稱爲stub,而服務器端的網絡服務類稱爲skeleton它會真正調用服務端的WorldClockService獲取結果,然後把結果通過網絡傳遞給客戶端
    • 整個過程由RMI底層負責實現序列化和反序列化
      在這裏插入圖片描述

Java實現RMI遠程調用2

  • 編寫服務端程序
  1. 第一步: 創建遠程接口
public interface WorldClock extends Remote {
	LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}

要求:

  1. 共享接口必須繼承java.rmi.Remote
  2. 所有方法必須拋出java.rmi.RemoteException 異常
  1. 第二步: 創建實現類
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
  1. 第三步: 註冊遠程服務,將服務通過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("啓動服務器");
    }
}

注意:

  1. 可以註冊多個服務 (可以對外暴露多個服務)
  2. URL的命名規則需要遵循,也就是和上面格式一模一樣
  • 編寫客戶端程序
  1. 第一步: 將接口複製到客戶端

    客戶端必須也擁有,一個和服務端一樣的接口, 並且這個接口所在的路徑 ,必須和服務端的接口一模一樣
    服務端

  2. 第二步: 使用服務器端暴露的接口獲取數據

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遠程調用弊端

  1. Java的RMI嚴重依賴序列化和反序列化 這可能會造成嚴重的安全漏洞
  2. 因爲Java的序列化和反序列化不但涉及到數據,還涉及到二進制的字節碼即使使用白名單機制也很難保證100%排除惡意構造的字節碼。
  3. 因此使用RMI時,雙方必須是內網互相信任的機器服務端不要把端口暴露在公網上作爲對外服務。
  4. 此外,Java的RMI調用機制決定了雙方必須是Java程序,其他語言很難調用Java的RMI如果要使用不同語言進行RPC調用,可以選擇更通用的協議,例如gRPC(grpc是谷歌的一個開源的rpc(遠程服務調用)框架(
  1. grpc是谷歌的一個開源的rpc(遠程服務調用)框架,可以讓各個語言按照指定的規則通過http2協議相互調用,這個規則是用Protocol Buffer(谷歌的一個數據描述語言)寫的一個.proto文件,grpc的目的就是爲了讓服務調用更方便。
  2. 目前支持的語言有C, C++,C#,Java, Node.js, Python,Go等,大部分語言都是通過插件根據.proto文件生成對應的代碼,用生成好的代碼,創建或調用grpc服務。
  3. gRPC和restful API都提供了一套通信機制,用於server/client模型通信,而且它們都使用http作爲底層的傳輸協議(嚴格地說, gRPC使用的http2.0,而restful api則不一定)

小結

  1. Java提供了RMI實現遠程方法調用:

  2. RMI通過自動生成stub和skeleton實現網絡調用客戶端只需要查找服務並獲得接口實例,服務器端只需要編寫實現類並註冊爲服務;

  3. RMI的序列化和反序列化可能會造成安全漏洞,因此調用雙方必須是內網互相信任的機器,服務端不要把端口暴露在公網上作爲對外服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章