目录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