RPC原理的探索及簡單案例

首先 先把RPC的概念擺出來 RPC基本概念

RPC是遠程過程調用(Remote Procedure Call)的縮寫形式。SAP系統RPC調用的原理其實很簡單,有一些類似於三層構架的C/S系統,第三方的客戶程序通過接口調用SAP內部的標準或自定義函數,獲得函數返回的數據進行處理後顯示或打印。

假如你的應用是一個單體應用,那麼你完全可以輕鬆的依賴本地函數調用來解決一切問題,而隨着業務和技術的發展,企業級別的系統不可能永遠停留在單體應用的層面,於是產生了分佈式系統架構,這個也促使RPC的誕生。

其實傳統的B/S架構的調用方式也能解決分佈式系統架構的問題,比如我在A服務暴露一個Restful接口,然後通過B服務通過http協議調用這個restful接口一樣可以實現分佈式系統的架構;但是問題來了,每次調用都需要寫一大串http請求的代碼,於是你可能會提出一個問題,能不能像本地調用一樣去調用遠程的服務並且讓用戶無感是調用的遠程服務呢?答案肯定是可以

RPC就是要解決這兩個問題:

  • 解決分佈式系統中 服務之間的調用問題
  • 遠程調用服務時 如何讓用戶無感 像調用本地一樣的方便

由於服務部署到不同的機器相互調用則避免不了網絡通信,而服務消費方在調用遠程服務時都要寫一大坨網絡通信的代碼這無疑是糟糕的體驗,那麼要讓通絡通信對使用者透明,我們需要對網絡通信的細節進行封裝,首先我們先看下RPC的調用流程:

 

  1. 服務消費方(client)調用以本地調用方式調用服務;
  2. client stub接收到調用後負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;
  3. client stub找到服務地址,並將消息發送到服務端;
  4. server stub收到消息後進行解碼;
  5. server stub根據解碼結果調用本地的服務;
  6. 本地服務執行並將結果返回給server stub;
  7. server stub將返回結果打包成消息併發送至消費方;
  8. client stub接收到消息,並進行解碼;
  9. 服務消費方得到最終結果。

RPC的目標就是要把2~8這些步驟都封裝起來對用戶透明化。那麼怎麼做才能封裝這些細節讓用戶像調用本地一樣調用遠程服務呢?java中有一種代理模式(dubbo採用的就是這種方式)可以解決這個問題,我們本地生成一個遠程服務的代理對象,將這個代理對象放進我們的容器內而在這個代理對象的內部去實現上述所說的對遠程服務的調用過程,由此 就可以像調用本地一樣調用遠程了。

 市面上已經有很多開源的RPC框架,比如阿里巴巴的Dubbo、Google 的gRPC等,這些現有的框架已經很完美的解決了我們上面聊到的問題,詳情可以去相應的官網瞭解具體用法。

理論的東西千篇一律 網上可以找到很多,真正要理解和加深概念還是需要自己學動手寫一下,下面我就把自己的理解轉換成一個簡單的demo

首先是client端發起RPC請求去調用遠程服務:

public class RPCConsumerApp {

    public static void main(String[] args) {

        Shop shop = new ShopImpl();

        String resp = shop.buy("頸椎病康復指南",1);

        System.out.println("購買結果:" + resp);

    }
}

/**
 * 商店
 */
interface Shop{

    /**
     * 購買商品
     * @param name
     * @param count
     * @return
     */
    public String buy(String name,int count);
}

/**
 * 商店實現類(使用代理模式)
 * 其真正的實現類應該是部署到遠端服務器上 本地只是個虛擬的實現類
 * 其中封裝了遠程調用接口的細節
 */
class ShopImpl implements Shop{

    //測試代碼 端口暫時寫死
    private final static int PORT = 9090;

    @Override
    public String buy(String name, int count) {

        String result = null;

        try{
            //根據服務名稱 獲取註冊中心服務列表 默認是 接口.方法
            List<String> providers = lookupProviders("Shop.buy");

            //根據負載均衡策略篩選提供服務的節點
            String providerAddress = chooseProvider(providers);

            //通訊協議可選擇 無論是http  還是socket 都可以
            Socket socket = new Socket(providerAddress,PORT);

            //將請求參數進行序列化
            ShopRequest shopRequest = new ShopRequest(name,count);

            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

            //將請求發送給服務提供者
            objectOutputStream.writeObject(shopRequest);

            //接受響應
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());

            Object resp = objectInputStream.readObject();
            result = resp.toString();
            System.out.println("購買結果爲:"+resp.toString());

        }catch (Exception e){
            e.printStackTrace();
            System.out.println("購買異常:"+e.getMessage());
        }
        return result;
    }


    /**
     * 獲取指定服務的服務實例列表
     *
     * 分佈式系統中會有多個服務節點提供服務 通常會有註冊中心來管理實例列表
     *
     * @return
     */
    public List<String> lookupProviders(String serveName){

        List<String> providers = new ArrayList<>();
        providers.add("127.0.0.1");

        return providers;
    }

    /**
     *
     * 負載均衡算法 根據服務實例列表 篩選具體服務實例節點提供服務
     * @return
     */
    public String chooseProvider(List<String> providers){
        return providers.get(0);
    }
}

/**
 * rpc 請求參數體
 * 包括請求的接口名 參數 等
 */
@Data
@NoArgsConstructor
class ShopRequest implements Serializable{


    private String method = "buy";

    private String name;

    private int count;

    public ShopRequest(String name, int count) {
        this.name = name;
        this.count = count;
    }
}

我們通過把RPC調用的細節進行封裝(可以採用代理的方式)客戶端使用遠程服務時就像是調用本地一樣方便

再看一下服務端是如果實現提供遠程服務的:

public class RPCServerApp {
    private final static int PORT = 9090;

    private Shop shop = new RealShopImpl();

    public static void main(String[] args) throws Exception{

        RPCServerApp rpcServerApp = new RPCServerApp();
        rpcServerApp.run();

    }

    public void run() throws Exception{
        ServerSocket serverSocket = new ServerSocket(PORT);
        try {

            //循環接受客戶端請求
            while (true){
                Socket socket = serverSocket.accept();
                try{
                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());

                    Object request = objectInputStream.readObject();
                    System.out.println("客戶端請求參數:"+request.toString());

                    String buyResult = null;
                    if(request instanceof ShopRequest){
                        ShopRequest shopRequest = (ShopRequest) request;
                        if("buy".equalsIgnoreCase(shopRequest.getMethod())){
                            buyResult = shop.buy(shopRequest.getName(),shopRequest.getCount());
                        }
                    }

                    ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                    objectOutputStream.writeObject(buyResult);

                }catch (Exception e){
                    System.out.println(e.getMessage());
                }finally {
                    socket.close();
                }
            }

        }catch (Exception e){
            System.out.println("exception :" + e.getMessage());
        }finally {
            serverSocket.close();
        }
    }
}

/**
 * 商店實現類
 */
class RealShopImpl implements Shop {

    @Override
    public String buy(String name, int count) {

        String result = null;

        try {
            result = "恭喜您!購買【" + name + "】成功,數量爲【" + count + "】";
        } catch (Exception e) {
            System.out.println("購買異常:" + e.getMessage());
            result = "商店打烊 暫時無法出售貨物";
        }
        return result;
    }
}

服務端接受客戶端的請求,對參數進行反序列化>執行本地處理>序列化執行結果並返回

我們的demo比較簡單 只是把自己的理解轉換成代碼,商用框架遠比這個複雜的多,我們以dubbo舉例,dubbo通過和spring的集成,在spring容器加載的時候便會加載我們使用 <dubbo:reference/>或者@Reference 胡姐配置的bean,爲這些配置的對象生成一個代理對象,這個代理對象會負責進行遠程通信調用遠程服務,我們所需要的就是將這些代理對象注入到我們的服務中使用便可。

那我們怎麼才能像dubbo那樣不用自己手寫代理對象而自動生成所需的代理對象呢?答案肯定是要遵循一套規範,我們要求所有遠程調用的服務都遵循一套模板,我們把調用遠程的所有信息放到一個RPCRequest對象裏面,發給遠程服務提供端,在服務端接收並解析之後他就知道我們到底想要調用哪個接口並且也知道我們傳過來的參數列表分別是什麼類型值是什麼,就像dubbo的RpcInvocation一樣:

public class RpcInvocation implements Invocation, Serializable {
    private static final long serialVersionUID = -4355285085441097045L;
    //方法名
    private String methodName;
    //參數類型
    private Class<?>[] parameterTypes;
    //參數值
    private Object[] arguments;
    private Map<String, String> attachments;
    private transient Invoker<?> invoker;
    private transient Class<?> returnType;
    private transient InvokeMode invokeMode;

一個好的RPC框架需要考慮的問題有很多,比如框架的通用性、通信的協議、服務端線程池、服務註冊中心、負載均衡、服務多版本控制等等一系列的問題

有興趣的可以研究一下dubbo的原理和源碼,相信會對你對RPC原理的理解有幫助

文章可能寫的不是特別深,現在也處於摸索學習階段 希望大佬們 能多提建議 促使我進步

 

 

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