RPC
解決的問題
RPC 主要是為了解決的兩個問題:
- 解決分散式系統中,服務之間的呼叫問題。
- 遠端呼叫時,要能夠像本地呼叫一樣方便,讓呼叫者感知不到遠端呼叫的邏輯。
這一節我們來學習下如何基於 websocket 實現最簡單的 rpc 呼叫,後續會實現基於 netty4 的版本。
開源地址: https://github.com/houbb/rpc
完整流程
其中左邊的Client,對應的就是前面的Service A,而右邊的Server,對應的則是Service B。
下面一步一步詳細解釋一下。
- Service A的應用層程式碼中,呼叫了Calculator的一個實現類的add方法,希望執行一個加法運算;
- 這個Calculator實現類,內部並不是直接實現計算器的加減乘除邏輯,而是通過遠端呼叫Service B的RPC介面,來獲取運算結果,因此稱之為Stub;
- Stub怎麼和Service B建立遠端通訊呢?這時候就要用到遠端通訊工具了,也就是圖中的Run-time Library,這個工具將幫你實現遠端通訊的功能,比如Java的Socket,就是這樣一個庫,當然,你也可以用基於Http協議的HttpClient,或者其他通訊工具類,都可以,RPC並沒有規定說你要用何種協議進行通訊;
- Stub通過呼叫通訊工具提供的方法,和Service B建立起了通訊,然後將請求資料發給Service B。需要注意的是,由於底層的網路通訊是基於二進位制格式的,因此這裡Stub傳給通訊工具類的資料也必須是二進位制,比如calculator.add(1,2),你必須把引數值1和2放到一個Request物件裡頭(這個Request物件當然不只這些資訊,還包括要呼叫哪個服務的哪個RPC介面等其他資訊),然後序列化為二進位制,再傳給通訊工具類,這一點也將在下面的程式碼實現中體現;
- 二進位制的資料傳到Service B這一邊了,Service B當然也有自己的通訊工具,通過這個通訊工具接收二進位制的請求;
- 既然資料是二進位制的,那麼自然要進行反序列化了,將二進位制的資料反序列化為請求物件,然後將這個請求物件交給Service B的Stub處理;
- 和之前的Service A的Stub一樣,這裡的Stub也同樣是個“假玩意”,它所負責的,只是去解析請求物件,知道呼叫方要調的是哪個RPC介面,傳進來的引數又是什麼,然後再把這些引數傳給對應的RPC介面,也就是Calculator的實際實現類去執行。很明顯,如果是Java,那這裡肯定用到了反射。
- 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
我是老馬,期待與你的下次重逢。