目錄html
對於分佈式系統而言,不一樣的服務分佈在不一樣的節點上,一個服務要完成本身的功能常常須要調用其餘服務的接口,好比典型的微服務架構。一般這種服務調用方式有兩種,一種是發送HTTP請求的方式,另外一種則是RPC的方式,RPC是Remote Procedure Call(遠程過程調用)的簡稱,可讓咱們像調用本地接口同樣使用遠程服務。相比HTTP調用,RPC的方式至少在如下幾個方面有優點java
正由於基於RPC方式的服務調用有着性能消耗低,傳輸效率高,更容易作負載均衡和服務治理的優勢,因此分佈式系統內部大多采用這種方式進行分佈式服務調用。可供選擇的RPC框架不少,好比Hession,Dubbo,Thrift這些很早就開源,平時項目中使用也不少。不過最近有一個叫gRPC的RPC框架很火,被使用在不少微服務相關的開源項目中,好比華爲的Apache ServiceComb Saga。這篇博客做爲我學習gRPC的入門筆記,只對它的核心概念和簡單用法作些介紹數組
gRPC是由Google開發並開源的RPC框架,它具備如下特色網絡
一個gRPC服務的大致架構能夠用官網上的一幅圖表示
數據結構
gRPC服務端使用C++構建,客戶端可使用Ruby或者Java構建,客戶端經過一個Stub存根(代理)對象發起RPC調用,請求和響應消息都使用Protocol Buffer進行序列化。架構
當咱們在微服務中使用gRPC時,整個服務調用過程以下所示(圖片來自網絡)
負載均衡
經過gRPC,遠程服務的調用對使用者更加簡單和透明,底層的傳輸方式,序列化方式,通訊細節等通通不須要關係,固然這些對其餘RPC框架而言也適用。框架
一個直觀的想法,在客戶端調用服務端提供的遠程接口前,雙方必須進行一些約定,好比接口的方法簽名,請求和響應的數據結構等,這個過程稱爲服務定義。服務定義須要特定的接口定義語言(IDL)來完成,gRPC中默認使用protocol buffers。它是google很早就開源的一款序列化框架,其定義了一種數據序列化協議,獨立於語言和平臺,提供了多種語言的實現:Java,C++,Go等,每一種實現都包含了相應語言的編譯器和庫文件。使用它進行服務定義須要編寫.proto後綴的IDL文件,並經過其編譯器生成特定語言的數據結構、服務端接口和客戶端Stub代碼。異步
消息是表示RPC接口的請求參數和響應結果的數據結構。以下定義了一個請求消息和響應消息maven
//定義請求消息的結構 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,具體的對應關係以下表所示
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 bool booly = 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是保留編號不能使用。這些字段編號在使用過程當中不能進行修改,不然會出現問題。
標題中的接口能夠類比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方法
rpc SayHello(HelloRequest) returns (HelloResponse){ }
客戶端發送一個請求,從服務端得到一個響應,整個過程就像一個本地的方法調用。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){ }
客戶端發送一個請求,並從服務端得到一個流(stream)。服務端能夠往流中寫入N個消息做爲響應,而且每一個消息能夠單獨發送,客戶端能夠從流中按順序讀取這些消息,以下圖所示(圖片來自網絡)
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
客戶端經過流發送一連串的多個請求,並等待從服務端返回的一個響應。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
客戶端經過流發送N個請求,服務端經過流發送N個響應,彼此相互獨立,而且讀寫沒有特定的次序要求,好比服務端能夠收到全部請求後再返回響應,也能夠每讀取一個或K個請求會返回響應。
該特性能夠充分利用HTTP/2.0的多路複用功能,實現了服務端和客戶端的全雙工通訊,以下圖所示(圖片來自網絡)
按照慣例,編寫一個gRPC版本的hello world來說解如何構建一個簡單的gRPC服務——客戶端發送一個請求,服務端返回一個響應。
好比
客戶端:takumiCX
服務端:Hello takumiCX
建立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; }
<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文件爲咱們生成了對應的類,以下圖所示
//擴展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(); } }
//服務要監聽的端口 int port=50051; //建立server對象,監聽端口,註冊服務並啓動 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步
完整代碼以下:
/** * @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客戶端的調用流程以下所示
先啓動gRPC服務端,而後啓動gRPC客戶單。客戶端發送gRPC請求takumiCX
,收到了來自服務端的響應Hello takumiCX
gRPC做爲開源RPC框架的新勢力,基於HTTP/2.0協議進行設計,使用高性能的Protocol Buffer進行消息的序列化,於是性能很是好,並且提供了完整的負載均衡和服務治理能力,加上其和語言無關、平臺無關的特色,很是適合做爲微服務內部服務間調用的選型。
《深刻淺出gRPC》 https://developers.google.com/protocol-buffers/ https://grpc.io/docs/guides/concepts.html#service-definition