java 從零開始手寫 RPC (01) 基於 websocket 實現 RPC 簡單實現 開源地址

RPC

解決的問題

RPC 主要是爲了解決的兩個問題:

  • 解決分佈式系統中,服務之間的調用問題。

  • 遠程調用時,要能夠像本地調用一樣方便,讓調用者感知不到遠程調用的邏輯。

這一節我們來學習下如何基於 websocket 實現最簡單的 rpc 調用,後續會實現基於 netty4 的版本。

開源地址: https://github.com/houbb/rpc

完整流程

其中左邊的Client,對應的就是前面的Service A,而右邊的Server,對應的則是Service B。

下面一步一步詳細解釋一下。

  1. Service A的應用層代碼中,調用了Calculator的一個實現類的add方法,希望執行一個加法運算;

  2. 這個Calculator實現類,內部並不是直接實現計算器的加減乘除邏輯,而是通過遠程調用Service B的RPC接口,來獲取運算結果,因此稱之爲Stub;

  3. Stub怎麼和Service B建立遠程通訊呢?這時候就要用到遠程通訊工具了,也就是圖中的Run-time Library,這個工具將幫你實現遠程通訊的功能,比如Java的Socket,就是這樣一個庫,當然,你也可以用基於Http協議的HttpClient,或者其他通訊工具類,都可以,RPC並沒有規定說你要用何種協議進行通訊;

  1. Stub通過調用通訊工具提供的方法,和Service B建立起了通訊,然後將請求數據發給Service B。需要注意的是,由於底層的網絡通訊是基於二進制格式的,因此這裏Stub傳給通訊工具類的數據也必須是二進制,比如calculator.add(1,2),你必須把參數值1和2放到一個Request對象裏頭(這個Request對象當然不只這些信息,還包括要調用哪個服務的哪個RPC接口等其他信息),然後序列化爲二進制,再傳給通訊工具類,這一點也將在下面的代碼實現中體現;

  2. 二進制的數據傳到Service B這一邊了,Service B當然也有自己的通訊工具,通過這個通訊工具接收二進制的請求;

  3. 既然數據是二進制的,那麼自然要進行反序列化了,將二進制的數據反序列化爲請求對象,然後將這個請求對象交給Service B的Stub處理;

  4. 和之前的Service A的Stub一樣,這裏的Stub也同樣是個“假玩意”,它所負責的,只是去解析請求對象,知道調用方要調的是哪個RPC接口,傳進來的參數又是什麼,然後再把這些參數傳給對應的RPC接口,也就是Calculator的實際實現類去執行。很明顯,如果是Java,那這裏肯定用到了反射。

  5. RPC接口執行完畢,返回執行結果,現在輪到Service B要把數據發給Service A了,怎麼發?一樣的道理,一樣的流程,只是現在Service B變成了Client,Service A變成了Server而已:Service B反序列化執行結果->傳輸給Service A->Service A反序列化執行結果 -> 將結果返回給Application,完畢。

簡單實現

假設服務 A,想調用服務 B 的一個方法。

因爲不在同一個內存中,無法直接使用。如何可以實現類似 Dubbo 的功能呢?

這裏不需要使用 HTTP 級別的通信,使用 TCP 協議即可。

common

公用模塊,定義通用對象。

  • Rpc 常量
public interface RpcConstant {

    /**
     * 地址
     */
    String ADDRESS = "127.0.0.1";

    /**
     * 端口號
     */
    int PORT = 12345;

}
  • 請求入參
public class RpcCalculateRequest implements Serializable {

    private static final long serialVersionUID = 6420751004355300996L;

    /**
     * 參數一
     */
    private int one;

    /**
     * 參數二
     */
    private int two;

    //getter & setter & toString()
}
  • 服務接口
public interface Calculator {

    /**
     * 計算加法
     * @param one 參數一
     * @param two 參數二
     * @return 返回結果
     */
    int add(int one, int two);

}

server

  • 服務接口的實現
public class CalculatorImpl implements Calculator {

    @Override
    public int add(int one, int two) {
        return one + two;
    }

}
  • 啓動服務
public static void main(String[] args) throws IOException {
    Calculator calculator = new CalculatorImpl();
    try (ServerSocket listener = new ServerSocket(RpcConstant.PORT)) {
        System.out.println("Server 端啓動:" + RpcConstant.ADDRESS + ":" + RpcConstant.PORT);
        while (true) {
            try (Socket socket = listener.accept()) {
                // 將請求反序列化
                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                Object object = objectInputStream.readObject();
                System.out.println("Request is: " + object);
                // 調用服務
                int result = 0;
                if (object instanceof RpcCalculateRequest) {
                    RpcCalculateRequest calculateRpcRequest = (RpcCalculateRequest) object;
                    result = calculator.add(calculateRpcRequest.getOne(), calculateRpcRequest.getTwo());
                }
                // 返回結果
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectOutputStream.writeObject(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

啓動日誌:

Server 端啓動:127.0.0.1:12345

client

  • 客戶端調用
public static void main(String[] args) {
    Calculator calculator = new CalculatorProxy();
    int result = calculator.add(1, 2);
    System.out.println(result);
}
  • 計算的代理類
public class CalculatorProxy implements Calculator {

    @Override
    public int add(int one, int two) {
        try {
            Socket socket = new Socket(RpcConstant.ADDRESS, RpcConstant.PORT);

            // 將請求序列化
            RpcCalculateRequest calculateRpcRequest = new RpcCalculateRequest(one, two);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

            // 將請求發給服務提供方
            objectOutputStream.writeObject(calculateRpcRequest);

            // 將響應體反序列化
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object response = objectInputStream.readObject();

            if (response instanceof Integer) {
                return (Integer) response;
            } else {
                throw new RuntimeException();
            }
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 調用日誌

client 端

3

server 端

Server 端啓動:127.0.0.1:12345
Request is: RpcCalculateRequest{one=1, two=2}

開源地址

爲了便於大家學習,以上源碼已經開源:

https://github.com/houbb/rpc

我是老馬,期待與你的下次重逢。

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