Rpc與RMI服務

前言

前面我們曾經深入的瞭解過Http協議,以及Https協議的思考,但是在日常開發中,還有這麼一種常見的技術–RPC,許多常見的框架庫都是基於RPC技術進行開發實現的進程通信的技術,例如RMI,gRpc以及dubbo等,因此在Java開發中,RPC也是常用技術中很重要的一環,本篇開始我們從RPC基礎開始逐步瞭解RPC技術以及經典RPC的實現–RMI的使用及原理

RPC概念

RPC全稱Inter-Process Communication ,即我們常說的進程間通信,指至少兩個進程間或者跨線程進行傳輸數據或者信號的技術。進程是計算機系統分配資源的最小單位,而每一個進程都有着自身獨立的系統資源,且彼此隔離。爲了讓不同的進程相互訪問並且能進行協調工作,纔有了進程間通信技術,這些進程可以運行在同一個計算機上或者在不同的網絡連接的計算機上。進程間通信技術包括信息傳遞、同步、共享內存以及能實現遠程調用,而IPC也是Unix通信機制 的一種標準。

而IPC通信常見的有如下兩種:

本地過程調用(LPC):LPC用在多任務操作系統中,使得同時運行的任務之間可以互相會話進行數據傳輸,這些任務之間共享內存空間使得任務同步和互相發送信息

遠程過程通信(RPC):RPC與LPC類似,其區別在於RPC僅進行網絡上進程間的傳輸通信,最開始出現RPC是sun公司和HP公司運行在UNIX操作系統的計算機中,一直演化發展至今

簡單RPC通信過程

需要明白的是RPC技術的核心並不在於使用什麼協議,RPC的目的僅僅是實現遠程調用,且對用戶透明,實現業務解耦。而一個簡單的RPC至少首先會使用動態代理技術,實現通信層的動態隔離,而傳輸過程中至少會選擇一種協議進行傳輸,將遠程的被調用方的方法的返回結果進行序列化傳輸,即可完成一個最簡單的RPC通信,可以參考spring remoting 和RMI,而一個複雜的RPC則是針對其中的細節進行優化擴展,能支持更多的能力和協議以及序列化等的選擇,甚至可以和第三方中間件結合使用,此類RPC實現可以參照dubbo,簡單來說:

1.RPC就是從一臺機器上通過參數傳輸的方式調用到另外一臺機器上的某個固定函數或者方法,並且按照固定的傳輸協議方式接受到返回的結果

2.RPC會隱藏底層的通信細節,並且RPC自身就是一個請求響應模型

3.客戶端發起請求後,服務端返回響應,RPC在使用方式上像調用本地函數一樣即可簡單實現遠程函數的調用

而RPC的通信過程簡單來說,可以如圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-t1krRNxR-1578468908860)(H:\十月文章\Rpc與RMI服務\img\RPC框架調用過程.png)]

RPC框架

首先我們來思考一個問題,一個RPC框架至少擁有哪些能力?我們可以從下面幾點來考慮:

遠程通信能力

首先RPC得定義就是能實現網絡端遠程服務器之間的通信,所以RPC一定要有的最基礎的能力,就是能發起遠程的網絡傳輸連接請求,即Sokect通訊的維護和協議能力封裝

Call ID映射能力

RPC框架既然擁有跨服務端進行傳輸數據交互的能力,那麼我們需要告知遠程服務器如何調用Multiply ,要知道在本地調用中,函數體是直接通過函數指針來指定的,而在遠程通信中,由於不在一個機器中,使用函數指針的方式是行不通的,所以在RPC框架中,幾乎所有的函數都會有一個自己的ID,且保持唯一,服務端和客戶端之間都會去維護ID在本機器中不同的地址,當有RPC請求傳遞過來的時候,服務端只需要查找本地維護的ID和地址的關係,找到對應的函數,進行調用,並且傳輸對應的結果即可

序列化/反序列化能力

當數據從遠程服務器上返回,由於遠程網絡傳輸的原因,在Java中必須使用序列化標準來實現數據的傳輸,不能像本地調用一樣簡單的將內存數據讀取,所以RPC框架一定要有序列化支持的能力,可以將遠程的數據序列化爲字節流,接受完畢後反序列化成對應的內存對象

跨平臺/誇語言交互能力

由於RPC遠程交互傳輸數據,這個時候服務端的系統環境以及所交互的進程使用的語言等並不是固定的,所以RPC框架需要擁有一個穩定的可以跨平臺的,跨語言環境的傳輸協議支持

常見的RPC框架

瞭解了RPC框架最基礎的能力以後,我們來看看Java開發中那些常見的經典RPC框架吧:

1.Netty:netty框架嚴格意義上並不屬於RPC,更多的是作爲一種網絡協議框架,能夠快速的提供高性能的RPC或者HTTP等的遠程通信能力基礎

2.bRPC:bRPC是一個基於protobuf接口的PRC框架,看到命名就可以知道此框架來自著名的百度團隊,此框架囊括了百度內部的所有RPC協議,並且支持多種第三方協議,由於基於protobuf算法,從性能來看幾乎是同類RPC框架中的領跑者

3.dubbo:dubbo框架是業界著名的阿里巴巴團隊早期開源的優秀的RPC框架,此框架發展較成熟,能獨立作爲商業化組件使用,且依託於Spring使用

4.gRPC:gRPC是谷歌團隊基於Netty開發實現的底層網絡庫,而此框架還有Go語言的版本,基於Net庫開發

5.RMI:rmi可以說是java中最早的RPC框架之一,且此rpc框架由sun團隊開發,集成於JDK中,可以說完全可以實現開箱即用,不需要任何外部jar依賴,但由於早期的rpc實現,且設計上並不是爲了解決互聯網企業類的高併發問題,所以不建議現在互聯網開發中作爲rpc實現

RMI框架基本使用

RMI既然是java團隊設計出來的rpc框架,雖然現在已經不適合企業級生產使用,但是其中的思想和規範值得學習,我們就來看看RMI框架如何使用吧:

RMI三大基本類

實現RMI所需要的API基本都在三大類中,如下:

java.rmi:提供客戶端需要的類、接口和異常;

java.rmi.server:提供服務端需要的類、接口和異常;

java.rmi.registry:提供註冊表的創建以及查找和命名遠程對象的類、接口和異常 ;

構建RMI服務端

首先在RMI中服務端供客戶端調用的實例稱之爲遠程對象,而在RMI中實現了java.rmi.Remote接口的類或者繼承了java.rmi.Remote接口的都是RMI的遠程對象。那麼我們來定義一個接口,繼承java.rmi.Remote

/**
 *用戶處理器
**/
public interface UserHandler extends Remote {
    String getUserName(int id) throws RemoteException;
    String getUserPassWord() throws RemoteException;
    User getUserByName(String name) throws RemoteException;
}

這裏需要注意的一點是,繼承了Remote接口的接口中定義的所有的方法必須拋出RemoteException異常,並且該接口的實現類必須直接或者間接繼承java.rmi.server.UnicastRemoteObject類,該類中提供了很多支持RMI的方法,可以通過JRMP協議導出一個遠程對象的引用,生成動態代理構建的Stub對象,實現代碼如下:

public class UserHandlerImpl extends UnicastRemoteObject implements UserHandler {
    //這裏因爲集繼承了UnicastRemoteObject類,其構造器要拋出RemoteException,所以申明構造
    public UserHandlerImpl() throws RemoteException {
        super();
    }

    @Override
    public String getUserName(int id) throws RemoteException {
        return "pdc";
    }
    @Override
    public String getUserPassWord() throws RemoteException{
        return 654321;
    }
    @Override
    public User getUserByName(String name) throws RemoteException{
        return new User(name, 654321);
    }
}

這裏我們構造了一個User實體,爲了能實現遠程傳輸,所以這裏我們將其進行序列化:

public class User implements Serializable {
    private static final long serialVersionUID = 42L;
    
    private String name;
    private String passWord;

    public String getName(){
        return this.name;
    }
    
    public String getPassWord(){
        return this.passWord;
    }
    
    public void setName(String name){
        this.name = name;
    }
    
    public void setPassWord(String passWord){
        this.passWord = passWord;
    }
    
    public User(String name, String passWord) {
        this.name = name;
        this.passWord = passWord;
    }
}

需要注意的一點是,如果jdk版本低於1.5,需要手動運行rmic命令生成實現類的Stub對象,而1.5開始使用動態代理技術,已經可以自動生成Stub對象了,做完這些就可以啓動服務端了:

UserHandler userHandler = null;
try {
    userHandler = new UserHandlerImpl();
    Naming.rebind("user", userHandler);//將當前的實例與名稱爲user綁定,後面客戶端調用查找對應的名稱
    System.out.println(" RMI 服務端啓動成功");
} catch (Exception e) {
    System.err.println(" RMI 服務端啓動失敗");
    e.printStackTrace();
}

構建RMI註冊表

其實所謂註冊表就是保存了RMI服務端啓動與綁定的名稱的進程,由於jdk已經把RMI代碼集成到了JDK中,RMI的註冊表其實不需要寫任何代碼,在JDK的bin目錄下已經存在一個叫rmiregistry.exe的程序,不過我們需要在當前的class類路徑下啓動註冊表(所以需要注意JAVA_HOME環境變量一定要配置成功) ,來到class類路徑下,輸入命令:

rmiregistry 9999

即可指定rmi的註冊表在9999端口中運行,如果不指定端口,默認使用1099,當然不想讓RMI的註冊表在前臺顯示,也可以輸入後臺運行命令:

start rmiregistry

構建RMI客戶端

前面服務端和註冊表都已經運行起來了,接下來我們需要的就是客戶端發起訪問的請求了,需要注意的是,User實例類和UserHandler接口在客戶端代碼中也有一份(企業開發過程中會依賴同一份代碼),所以這裏的客戶端調用代碼如下:

try {
    UserHandler handler = (UserHandler) Naming.lookup("user");//這裏使用的user是服務端啓動的時候綁定的名稱
    String passWord = handler.getUserPassWord();
    String name = handler.getUserName(1);
    System.out.println("name: " + name);
    System.out.println("passWord: " + passWord);
    System.out.println("user: " + handler.getUserByName("pdc"));
} catch (Exception e) {
    e.printStackTrace();
}

這樣就可以獲取到服務端的遠程對象的信息了,當然這裏有兩點需要注意:

1.這裏的UserHandler實體類和服務端的UserHandler接口所在的包名需要一致,即使用的限定全類名需要一致,否則會報如下的錯誤:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qvwi6s4E-1578468908862)(H:\十月文章\Rpc與RMI服務\img\userHandler包名不一致導致的報錯.png)]

2.我們這裏獲取的User實例屬於引用類型,需要注意的是獲取到的User實例對象也必須和服務端的包名一致,即限定全類名相同,否則,handler.getUserByName(“pdc”)方法調用的結果會報錯,如下所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lXVqH30B-1578468908864)(H:\十月文章\Rpc與RMI服務\img\User包名不一致導致報錯.png)]

自定義啓動RMI註冊表

在本篇的結尾,我們來個彩蛋,還記得上面RMI的註冊表嗎?前面我們是通過JDK的exe程序啓動的,那麼我們能不能自己開發或者自己啓動RMI註冊表呢?其實是可以的,在java.rmi.registry包中有個Registry接口,並且該接口有個默認的實現類LocateRegistry,其實JDK源碼中Naming類就是使用的LocateRegistry實現的註冊和調用,那麼我們來看看LocateRegistry的方法:

createRegistry(int port)
createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
getRegistry()
getRegistry(int port)
getRegistry(String host)
getRegistry(String host, int port)
getRegistry(String host, int port, RMIClientSocketFactory csf)

可以看到這裏有兩個創建註冊表的方法,一個只有端口,開啓的默認是本機的註冊表,另外一個是可以輸入ip,端口,以及一些連接策略的自定義註冊表,還有幾個獲取註冊表的方法,很明顯這裏提供了註冊表的創建和調用的方法,同樣的我們之前的服務端代碼只要稍微改動一下,如下:

UserHandler userHandler = null;
Registry registry = LocateRegistry.createRegistry(9999);;
try {
    userHandler = new UserHandlerImpl();
    registry.rebind("user", userHandler);//將當前的實例與名稱爲user綁定,後面客戶端調用查找對應的名稱
    System.out.println(" RMI 服務端啓動成功");
} catch (Exception e) {
    System.err.println(" RMI 服務端啓動失敗");
    e.printStackTrace();
}

很明顯申明一下註冊表,並且使用註冊表替換Naming來綁定服務實例即可,客戶端亦是如此,修改後的代碼如下:

try {
    
    Registry registry=LocateRegistry.getRegistry("127.0.0.1",9999);
    UserHandler handler = (UserHandler) registry.lookup("user");//這裏使用的user是服務端啓動的時候綁定的名稱
    String passWord = handler.getUserPassWord();
    String name = handler.getUserName(1);
    System.out.println("name: " + name);
    System.out.println("passWord: " + passWord);
    System.out.println("user: " + handler.getUserByName("pdc"));
} catch (Exception e) {
    e.printStackTrace();
}

這樣就完成了和之前一樣的服務發佈與調用過程了

如果喜歡本文,可以關注我們的官方賬號,第一時間獲取資訊。
你的關注是對我們更新最大的動力哦~

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