1. RPC 入門
1.1 RPC 架構原理
RPC 架構的目标就是讓遠端服務調用更加簡單、透明,RPC 架構負責屏蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進制)和通信細節。服務調用者可以像調用本地接口一樣調用遠端的服務提供者,而不需要關心底層通信細節和調用過程。
RPC 架構的調用原理圖如下所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL90TUOd3aE1EerRVZzplMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL3ADN4IjNzATMxAjMwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
整理成序列圖後的效果是:
RPC 架構--時序圖
1.2 業界主流的 RPC 架構
業界主流的 RPC 架構整體上分為三類:
- 支援多語言的 RPC 架構,比較成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
- 隻支援特定語言的 RPC 架構,例如新浪微網誌的 Motan;
- 支援服務治理等服務化特性的分布式服務架構,其底層核心仍然是 RPC 架構, 例如阿裡的 Dubbo。
随着微服務的發展,基于語言中立性原則建構微服務,逐漸成為一種主流模式,例如對于後端并發處理要求高的微服務,比較适合采用 Go 語言建構,而對于前端的 Web 界面,則更适合 Java 和 JavaScript。
是以,基于多語言的 RPC 架構來建構微服務,是一種比較好的技術選擇。例如 Netflix,API 服務編排層和後端的微服務之間就采用 gRPC 進行通信。
2. gRPC
gRPC是一個高性能、開源和通用的 RPC 架構,支援多語言。
gRPC 基于 HTTP/2 标準設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接配接上的多複用請求等特性。這些特性使得其在移動裝置上表現更好,更省電和節省空間占用。
gRPC 調用模型
1、用戶端(gRPC Stub)調用 A 方法,發起 RPC 調用。
2、對請求資訊使用 Protobuf 進行對象序列化壓縮(IDL)。
3、服務端(gRPC Server)接收到請求後,解碼請求體,進行業務邏輯處理并傳回。
4、對響應結果使用 Protobuf 進行對象序列化壓縮(IDL)。
5、用戶端接受到服務端響應,解碼請求體。回調被調用的 A 方法,喚醒正在等待響應(阻塞)的用戶端調用并傳回響應結果。
- 一個RPC架構大緻需要動态代理、序列化、網絡請求、網絡請求接受(netty實作)、動态加載、反射這些知識點。現在開源及各公司自己造的RPC架構層出不窮,唯有掌握原理是一勞永逸的。
gRPC 是什麼?
在 gRPC 裡用戶端應用可以像調用本地對象一樣直接調用另一台不同的機器上服務端應用的方法,使得您能夠更容易地建立分布式應用和服務。與許多 RPC 系統類似,gRPC 也是基于以下理念:定義一個服務,指定其能夠被遠端調用的方法(包含參數和傳回類型)。在服務端實作這個接口,并運作一個 gRPC 伺服器來處理用戶端調用。在用戶端擁有一個存根能夠像服務端一樣的方法。
gRPC 特點
- 語言中立,支援多種語言;
- 基于 IDL ( 接口定義語言(Interface Define Language))檔案定義服務,通過 proto3 工具生成指定語言的資料結構、服務端接口以及用戶端 Stub;
- 通信協定基于标準的 HTTP/2 設計,支援·雙向流、消息頭壓縮、單 TCP 的多路複用、服務端推送等特性,這些特性使得 gRPC 在移動端裝置上更加省電和節省網絡流量;
- 序列化支援 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化架構,基于 HTTP/2 + PB, 保障了 RPC 調用的高性能。
gRPC原則和訴求
服務而非對象、消息而非引用 —— 促進微服務的系統間粗粒度消息互動設計理念,同時避免分布式對象的陷阱和分布式計算的謬誤。
普遍并且簡單 —— 該基礎架構應該在任何流行的開發平台上适用,并且易于被個人在自己的平台上建構。它在CPU和記憶體有限的裝置上也應該切實可行。
免費并且開源 —— 所有人可免費使用基本特性。以友好的許可協定開源方式釋出所有傳遞件。
互通性 —— 該資料傳輸協定(Wire Protocol)必須遵循普通網際網路基礎架構。
通用并且高性能 —— 該架構應該适用于絕大多數用例場景,相比針對特定用例的架構,該架構隻會犧牲一點性能。
分層的 —— 該架構的關鍵是必須能夠獨立演進。對資料傳輸格式(Wire Format)的修改不應該影響應用層。
負載無關的 —— 不同的服務需要使用不同的消息類型和編碼,例如protocol buffers、JSON、XML和Thrift,協定上和實作上必須滿足這樣的訴求。類似地,對負載壓縮的訴求也因應用場景和負載類型不同而不同,協定上應該支援可插拔的壓縮機制。
流 —— 存儲系統依賴于流和流控來傳遞大資料集。像語音轉文本或股票代碼等其它服務,依靠流表達時間相關的消息序列。
阻塞式和非阻塞式 —— 支援異步和同步處理在用戶端和服務端間互動的消息序列。這是在某些平台上縮放和處理流的關鍵。
取消和逾時 —— 有的操作可能會用時很長,用戶端運作正常時,可以通過取消操作讓服務端回收資源。當任務因果鍊被追蹤時,取消可以級聯。用戶端可能會被告知調用逾時,此時服務就可以根據用戶端的需求來調整自己的行為。
Lameducking —— 服務端必須支援優雅關閉,優雅關閉時拒絕新請求,但繼續處理正在運作中的請求。
流控 —— 在用戶端和服務端之間,計算能力和網絡容量往往是不平衡的。流控可以更好的緩沖管理,以及保護系統免受來自異常活躍對端的拒絕服務(DOS)攻擊。
可插拔的 —— 資料傳輸協定(Wire Protocol)隻是功能完備API基礎架構的一部分。大型分布式系統需要安全、健康檢查、負載均衡和故障恢複、監控、跟蹤、日志等。實作上應該提供擴充點,以允許插入這些特性和預設實作。
API擴充 ——可能的話,在服務間協作的擴充應該最好使用接口擴充,而不是協定擴充。這種類型的擴充可以包括健康檢查、服務内省、負載監測和負載均衡配置設定。
中繼資料交換 —— 常見的橫切關注點,如認證或跟蹤,依賴資料交換,但這不是服務公共接口中的一部分。部署依賴于他們将這些特性以不同速度演進到服務暴露的個别API的能力。
标準化狀态碼 —— 用戶端通常以有限的方式響應API調用傳回的錯誤。應該限制狀态代碼名字空間,使得這些錯誤處理決定更清晰。如果需要更豐富的特定域的狀态,可以使用中繼資料交換機制來提供。
gRPC的簡單例子
業務流程:Client端通過遠端RPC調用Server的擷取時間的接口,進而将伺服器時間擷取到本地并顯示。
1. 編寫.proto的服務定義檔案
syntax = "proto3”; // 文法版本
// stub選項
option java_package = "com.fly.grpc.api”;
option java_outer_classname = “RPCDateServiceApi”;
option java_multiple_files = true;
// 定義包名
package com.hansonwang99.grpc.api;
// 服務接口定義,服務端和用戶端都要遵守該接口進行通信
service RPCDateService {
rpc getDate (RPCDateRequest) returns (RPCDateResponse) {}
}
// 定義消息(請求)
message RPCDateRequest {
string userName = 1;
}
// 定義消息(響應)
message RPCDateResponse {
string serverDate = 1;
}
- 執行
指令來自動生成代碼Stubmvn compile
mvn編譯完成以後,在
target/generated-sources
目錄下就能看到根據上面
.proto
檔案自動轉化生成的
Java代碼Stub
2. gRPC服務端
實作gRPC服務接口
// 擷取時間
public class RPCDateServiceImpl extends RPCDateServiceGrpc.RPCDateServiceImplBase{
@Override
public void getDate(RPCDateRequest request, StreamObserver<RPCDateResponse> responseObserver) {
RPCDateResponse rpcDateResponse = null;
Date now=new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("今天是"+"yyyy年MM月dd日 E kk點mm分”);
String nowTime = simpleDateFormat.format( now );
try {
rpcDateResponse = RPCDateResponse
.newBuilder()
.setServerDate( "Welcome " + request.getUserName() + ", " + nowTime )
.build();
} catch (Exception e) {
responseObserver.onError(e);
} finally {
responseObserver.onNext( rpcDateResponse );
}
responseObserver.onCompleted();
}
}
gRPC服務端啟動類
public class GRPCServer {
private static final int port = 9999;
public static void main( String[] args ) throws Exception {
Server server = ServerBuilder.
forPort(port)
.addService( new RPCDateServiceImpl() )
.build().start();
System.out.println( "grpc服務端啟動成功, 端口=" + port );
server.awaitTermination();
}
}
3. gRPC用戶端
gRPC用戶端啟動類
public class GRPCClient {
private static final String host = “localhost”;
private static final int serverPort = 9999;
public static void main( String[] args ) throws Exception {
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress( host, serverPort ).usePlaintext().build();
try {
RPCDateServiceGrpc.RPCDateServiceBlockingStub rpcDateService = RPCDateServiceGrpc.newBlockingStub( managedChannel );
RPCDateRequest rpcDateRequest = RPCDateRequest
.newBuilder()
.setUserName(“hansonwang99”)
.build();
RPCDateResponse rpcDateResponse = rpcDateService.getDate( rpcDateRequest );
System.out.println( rpcDateResponse.getServerDate() );
} finally {
managedChannel.shutdown();
}
}
}