gRPC初探——概念介紹以及如何構建一個簡單的gRPC服務
目錄
引言
對於分散式系統而言,不同的服務分佈在不同的節點上,一個服務要完成自己的功能經常需要呼叫其他服務的介面,比如典型的微服務架構。通常這種服務呼叫方式有兩種,一種是傳送HTTP請求的方式,另一種則是RPC的方式,RPC是Remote Procedure Call(遠端過程呼叫)的簡稱,可以讓我們像呼叫本地介面一樣使用遠端服務。相比HTTP呼叫,RPC的方式至少在以下幾個方面有優勢
- 傳輸效率
RPC可以自定義TCP報文,基於TCP協議進行通訊,比如dubbo;同時也支援使用HTTP2協議進行通訊,比如gRPC。這相比傳統的HTTP1.1協議報文體積會更小,傳輸效率會更高。 - 效能消耗
RPC框架通常自帶高效的序列化機制,序列化和反序列化耗時更低,序列化後的位元組數通常也更小。 - 負責均衡
RPC框架通常自帶負載均衡策略,而HTTP請求要做負載均衡需要外部應用如Nginx的支援。 - 服務治理
下游服務新增,重啟,下線時能自動通知上游使用者,而HTTP的方式需要事先通知並修改相關配置。
正因為基於RPC方式的服務呼叫有著效能消耗低,傳輸效率高,更容易做負載均衡和服務治理的優點,所以分散式系統內部大多采用這種方式進行分散式服務呼叫。可供選擇的RPC框架很多,比如Hession,Dubbo,Thrift這些很早就開源,平時專案中使用也很多。不過最近有一個叫gRPC的RPC框架很火,被使用在很多微服務相關的開源專案中,比如華為的Apache ServiceComb Saga。這篇部落格作為我學習gRPC的入門筆記,只對它的核心概念和簡單用法做些介紹
1. gRPC簡介
gRPC是由Google開發並開源的RPC框架,它具有以下特點
- 語言中立
支援C,Java,Go等多種語言來構建RPC服務,這是gRPC被廣泛的應用在微服務專案中的重要原因,因為不同的微服務可能用不同的語言構建。 - 基於HTTP/2協議
支援雙向流,訊息頭壓縮,單TCP的多路複用,服務端推送等,這些特性使得gRPC更加適用於移動場景下的客戶端和服務端之間的通訊。 - 基於IDL定義服務
編寫.proto檔案即可生成特定語言的資料結構、服務端介面和客戶端Stub。 - 支援Protocol Buffer序列化
Protocol Buffer是由Google開發的一種資料序列化協議(類似於XML、JSON、Hession),平臺無關,壓縮和傳輸效率高,語法簡單,表達能力強。
一個gRPC服務的大體架構可以用官網上的一幅圖表示

gRPC服務端使用C++構建,客戶端可以使用Ruby或者Java構建,客戶端通過一個Stub存根(代理)物件發起RPC呼叫,請求和響應訊息都使用Protocol Buffer進行序列化。
當我們在微服務中使用gRPC時,整個服務呼叫過程如下所示(圖片來自網路)

通過gRPC,遠端服務的呼叫對使用者更加簡單和透明,底層的傳輸方式,序列化方式,通訊細節等統統不需要關係,當然這些對其他RPC框架而言也適用。
2. 使用Protocol Buffers進行服務定義
一個直觀的想法,在客戶端呼叫服務端提供的遠端介面前,雙方必須進行一些約定,比如介面的方法簽名,請求和響應的資料結構等,這個過程稱為服務定義。服務定義需要特定的介面定義語言(IDL)來完成,gRPC中預設使用protocol buffers。它是google很早就開源的一款序列化框架,其定義了一種資料序列化協議,獨立於語言和平臺,提供了多種語言的實現:Java,C++,Go等,每一種實現都包含了相應語言的編譯器和庫檔案。使用它進行服務定義需要編寫.proto字尾的IDL檔案,並通過其編譯器生成特定語言的資料結構、服務端介面和客戶端Stub程式碼。
2.1 定義訊息
訊息是表示RPC介面的請求引數和響應結果的資料結構。如下定義了一個請求訊息和響應訊息
//定義請求訊息的結構 message SearchResponse { // repeated表示該欄位可以重複任意次,等價於陣列:Result[] repeated Result result = 1; } //定義響應訊息的結構 message Result { //required表示該欄位的值恰好為1個 required string url = 1; //optional表示該欄位的值為0或1個 optional string title = 2; repeated string snippets = 3; }
定義訊息的關鍵字為message,相當於java中的class關鍵字,一個訊息就相當於java中的一個類。訊息內可以有多個欄位,欄位的型別可以分類如下
-
基本資料型別
int32表示java中的int,int64表示java中的long,string表示java中的string,具體的對應關係如下表所示
- 複雜資料型別
列舉,map等。
enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; }
map<key_type, value_type> map_field = N;
和java中類中可以定義類一樣,Protocol Buffers中訊息內也可以定義訊息,形成多層的巢狀結構
message Outer {// Level 0 message MiddleAA {// Level 1 message Inner {// Level 2 required int64 ival = 1; optional boolbooly = 2; } }
關於訊息定義,有幾點需要注意的地方
1.訊息中的欄位前可以有修飾符,修飾符主要有三種
-
required
required int64 ival = 1;
該欄位的值恰好只有一個,沒有或傳入多個都將報錯。
-
optional
optional int32 result_per_page = 3 [default = 10];
該欄位的值有0個或1個,傳入多個將報錯。且以optional修飾的欄位可以設定預設值,若沒有設定,則編譯器會根據型別自動設定一個預設值,比如string設定為空字串,bool型別設定為false等。
-
repeated
repeated int32 samples = 4
該欄位相當於java中的陣列,可以有0個或多個值。
2.訊息中的欄位有唯一編號,如下所示

這個唯一編號用來在訊息的二進位制格式中進行欄位的區分,範圍從1-229 - 1,其中19000-19999是保留編號不能使用。這些欄位編號在使用過程中不能進行修改,否則會出現問題。
2.2 定義服務介面
標題中的介面可以類比java中的Interface,內部可以有多個方法。gRPC中使用service關鍵定義服務介面
service HelloService { rpc SayHello (HelloRequest) returns (HelloResponse); } message HelloRequest { string greeting = 1; } message HelloResponse { string reply = 1; }
該服務介面HelloService內部只有一個rpc方法SayHello,請求引數為HelloRequest,響應結果為HelloResponse。
grpc中可以定義4中型別的rpc方法
- 1.簡單rpc方法
rpc SayHello(HelloRequest) returns (HelloResponse){ }
客戶端傳送一個請求,從服務端獲得一個響應,整個過程就像一個本地的方法呼叫。
- 2.服務端流式響應的rpc方法
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){ }
客戶端傳送一個請求,並從服務端獲得一個流(stream)。服務端可以往流中寫入N個訊息作為響應,並且每個訊息可以單獨傳送,客戶端可以從流中按順序讀取這些訊息,如下圖所示(圖片來自網路)

- 3.客戶端流式請求的rpc方法
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
客戶端通過流傳送一連串的多個請求,並等待從服務端返回的一個響應。
- 4.雙向流式rpc方法
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
客戶端通過流傳送N個請求,服務端通過流傳送N個響應,彼此相互獨立,並且讀寫沒有特定的次序要求,比如服務端可以收到所有請求後再返回響應,也可以每讀取一個或K個請求會返回響應。
該特性可以充分利用HTTP/2.0的多路複用功能,實現了服務端和客戶端的全雙工通訊,如下圖所示(圖片來自網路)

3.構建簡單的gRPC服務
按照慣例,編寫一個gRPC版本的hello world來講解如何構建一個簡單的gRPC服務——客戶端傳送一個請求,服務端返回一個響應。
比如
客戶端:takumiCX
服務端: Hello takumiCX
3.1 編寫proto檔案,定義訊息和介面
-
建立proto檔案
- 定義訊息和介面
//Protocal Buffers的版本有v2和v3之分,語法有較多變化,且相互不相容 //這裡使用的v3版本的 syntax = "proto3"; //編譯後生成的訊息類HelloRequest和HelloReply是否分別放在單獨的class檔案中 option java_multiple_files = true; //生成程式碼的包路徑 option java_package = "com.takumiCX.greeter"; //最外層的類名稱 option java_outer_classname = "HelloWorldProto"; //包名稱空間 package helloworld; // 服務介面 service Greeter { // 一個簡單的rpc方法 rpc SayHello (HelloRequest) returns (HelloReply) {} } // 請求訊息 message HelloRequest { string name = 1; } // 響應訊息 message HelloReply { string message = 1; }
3.2 通過maven外掛生成相應程式碼
- pom檔案配置如下
<dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.16.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.16.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.16.1</version> </dependency> </dependencies> <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.5.0.Final</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.1</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.16.1:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
在target目錄下可以看到編譯器通過編譯proto檔案為我們生成了對應的類,如下圖所示

3.3 gRPC服務端建立
- 第一步:首先要建立一個具體的服務介面實現類GreeterImpl,通過擴充套件gRPC為我們自動生成的服務抽象類GreeterGrpc.GreeterImplBase
/** * <pre> * 服務介面 * </pre> */ public static abstract class GreeterImplBase implements io.grpc.BindableService { /** * <pre> * 一個簡單的rpc方法 * </pre> */ public void sayHello(com.takumiCX.greeter.HelloRequest request, io.grpc.stub.StreamObserver<com.takumiCX.greeter.HelloReply> responseObserver) { asyncUnimplementedUnaryCall(getSayHelloMethod(), responseObserver); } @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) .addMethod( getSayHelloMethod(), asyncUnaryCall( new MethodHandlers< com.takumiCX.greeter.HelloRequest, com.takumiCX.greeter.HelloReply>( this, METHODID_SAY_HELLO))) .build(); } }
- 建立服務端物件,監聽特定埠,註冊具體的服務實現類並啟動
//服務要監聽的埠 int port=50051; //建立服務物件,監聽埠,註冊服務並啟動 Server server = ServerBuilder. forPort(port)//監聽50051埠 .addService(new GreeterImpl()) //註冊服務 .build()//建立Server物件 .start(); //啟動 log.info("Server started,listening on "+port); server.awaitTermination();
完整程式碼如下
/** * @author: takumiCX * @create: 2018-12-01 **/ public class HelloWorldServer { private static final Logger log=Logger.getLogger(HelloWorldServer.class.getName()); //擴充套件gRPC自動生成的服務介面,實現業務功能 static class GreeterImpl extends GreeterGrpc.GreeterImplBase{ @Override public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) { //構建響應訊息,從請求訊息中獲取姓名,在前面拼接上"Hello " HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); //在流關閉或丟擲異常前可以呼叫多次 responseObserver.onNext(reply); //關閉流 responseObserver.onCompleted(); } } public static void main(String[] args) throws IOException, InterruptedException { //服務要監聽的埠 int port=50051; //建立服務物件,監聽埠,註冊服務並啟動 Server server = ServerBuilder. forPort(port)//監聽50051埠 .addService(new GreeterImpl()) //註冊服務 .build()//建立Server物件 .start(); //啟動 log.info("Server started,listening on "+port); server.awaitTermination(); } }
gRPC的服務端建立過程如下所示(圖片來自網路)

3.5 gRPC客戶端建立
整個過程可以分為3步
- 1.根據服務端的ip和埠號,建立ManagedChannel
- 2.建立供客戶端使用的stub物件,可以建立兩種型別的stub,一種進行同步呼叫,一種進行非同步呼叫,後者發起呼叫的業務執行緒不會同步阻塞。
- 3.通過stub物件發起rpc呼叫,獲取服務端響應。
完整程式碼如下:
/** * @author: takumiCX * @create: 2018-12-01 **/ public class HelloWorldClient { private static final Logger log=Logger.getLogger(HelloWorldClient.class.getName()); public static void main(String[] args) { String host="localhost"; int port=50051; //1.建立ManagedChannel,繫結服務端ip地址和埠 ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext() .build(); //2.獲得同步呼叫的stub物件 GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel); ////獲得非同步呼叫的stub物件 //GreeterGrpc.GreeterFutureStub futureStub = GreeterGrpc.newFutureStub(channel); Scanner scanner = new Scanner(System.in); while (true){ //從控制檯讀取使用者輸入 String name = scanner.nextLine().trim(); //構建請求訊息 HelloRequest helloRequest = HelloRequest.newBuilder().setName(name).build(); //通過stub代理物件進行服務呼叫,獲取服務端響應 HelloReply helloReply = stub.sayHello(helloRequest); final String message = helloReply.getMessage(); log.warning("Greeting: "+message); } } }
gRPC客戶端的呼叫流程如下所示

3.6 測試
先啟動gRPC服務端,然後啟動gRPC客戶單。客戶端傳送gRPC請求 takumiCX
,收到了來自服務端的響應 Hello takumiCX
4. 總結
gRPC作為開源RPC框架的新勢力,基於HTTP/2.0協議進行設計,使用高效能的Protocol Buffer進行訊息的序列化,因而效能非常好,而且提供了完整的負載均衡和服務治理能力,加上其和語言無關、平臺無關的特點,非常適合作為微服務內部服務間呼叫的選型。
5. 參考資料
《深入淺出gRPC》
https://grpc.io
https://grpc.io/docs/guides/concepts.html#service-definition