目录
1. gRPC 简介
2. 使用 Protocol Buffers 进行服务定义
2.1 定义消息
2.2 定义服务接口
3. 构建简单的 gRPC 服务
3.1 编写 proto 文件, 定义消息和接口
3.2 通过 maven 插件生成相应代码
3.3 gRPC 服务端创建
3.5 gRPC 客户端创建
3.6 测试
4. 总结
5. 参考资料
对于分布式系统而言, 不同的服务分布在不同的节点上, 一个服务要完成自己的功能经常需要调用其他服务的接口, 比如典型的微服务架构. 通常这种服务调用方式有两种, 一种是发送 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 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 是保留编号不能使用. 这些字段编号在使用过程中不能进行修改, 否则会出现问题.
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, 重写服务方法
- // 扩展 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();
- }
- }
创建 server 对象, 监听特定端口, 注册具体的服务实现类并启动
- // 服务要监听的端口
- 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.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://developers.google.com/protocol-buffers/
- https://grpc.io/docs/guides/concepts.html#service-definition
来源: https://www.cnblogs.com/takumicx/p/10059448.html