java 從零開始手寫 RPC (01) 基於 websocket 實現

老馬嘯西風 發表於 2021-10-09
Java WebSocket

RPC

解決的問題

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

  • 解決分散式系統中,服務之間的呼叫問題。
  • 遠端呼叫時,要能夠像本地呼叫一樣方便,讓呼叫者感知不到遠端呼叫的邏輯。

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

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

完整流程

2018-08-24-rpc.png

其中左邊的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並沒有規定說你要用何種協議進行通訊;
  4. Stub通過呼叫通訊工具提供的方法,和Service B建立起了通訊,然後將請求資料發給Service B。需要注意的是,由於底層的網路通訊是基於二進位制格式的,因此這裡Stub傳給通訊工具類的資料也必須是二進位制,比如calculator.add(1,2),你必須把引數值1和2放到一個Request物件裡頭(這個Request物件當然不只這些資訊,還包括要呼叫哪個服務的哪個RPC介面等其他資訊),然後序列化為二進位制,再傳給通訊工具類,這一點也將在下面的程式碼實現中體現;
  5. 二進位制的資料傳到Service B這一邊了,Service B當然也有自己的通訊工具,通過這個通訊工具接收二進位制的請求;
  6. 既然資料是二進位制的,那麼自然要進行反序列化了,將二進位制的資料反序列化為請求物件,然後將這個請求物件交給Service B的Stub處理;
  7. 和之前的Service A的Stub一樣,這裡的Stub也同樣是個“假玩意”,它所負責的,只是去解析請求物件,知道呼叫方要調的是哪個RPC介面,傳進來的引數又是什麼,然後再把這些引數傳給對應的RPC介面,也就是Calculator的實際實現類去執行。很明顯,如果是Java,那這裡肯定用到了反射。
  8. 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

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

SIGN.png