RPC
解決的問題
RPC 主要是爲了解決的兩個問題:
解決分佈式系統中,服務之間的調用問題。
遠程調用時,要能夠像本地調用一樣方便,讓調用者感知不到遠程調用的邏輯。
這一節我們來學習下如何基於 websocket 實現最簡單的 rpc 調用,後續會實現基於 netty4 的版本。
完整流程
其中左邊的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}
開源地址
爲了便於大家學習,以上源碼已經開源:
我是老馬,期待與你的下次重逢。