gRPC 的特性及开发示例

1 gRPC 简介

gRPC 是 Google 于 2015 年对外开源的一款语言中立、平台中立的高性能 RPC 框架,可以在任何环境中运行。gRPC 支持负载平衡、链路跟踪、运行状况检查和身份验证,并且这些特性是可插拔的,它可以有效地连接数据中心和跨数据中心的服务,此外它还适用于分布式计算场景,支持将设备、移动应用程序和浏览器连接到后端服务。

2 gRPC 特性

Dubbo 不同,gRPC 更注重 RPC 本身的能力,除了上述提到的链路追踪等可插拔能力,并没有支持过多的服务治理能力,但其在 RPC 调用上有许多优点:

  1. gRPC 采用了 Google 自主研发的 ProtoBuf 作为序列化的解决方案,它解决了 gRPC 的跨语言调用的问题。由于定制了对应的 IDL,规范了接口定义,ProtoBuf 可以针对一些场景做性能优化,所以其序列化性能非常高。
  2. gRPC 采用的传输层协议是 HTTP/2,HTTP/2 相较于 HTTP 1.1 有许多优点:
    • 在性能上有更大的提升;
    • 支持流式通信 (Streaming Communication);
    • 安全性更高,天然支持 SSL,且具备成熟的权限功能。
    • HTTP/2 引入了服务器推送,即服务端向客户端发送比客户端请求更多的数据。这允许服务器直接提供浏览器渲染页面所需资源,而无须浏览器在收到、解析页面后再提起一轮请求,节约了加载时间。
  3. gRPC 的权限验证机制丰富,它内置了以下权限验证机制:
    • SSL/TLS:gRPC 具有 SSL/TLS 认证的集成,使用 SSL/TLS 认证服务器并加密客户端与服务器之间交换的所有数据。客户端可以使用可选机制来提供用于双向校验的证书;
    • ALTS (应用层传输安全):gRPC 支持 ALTS 以验证服务之间的通信,保证传输中的数据的安全。它是由 Google 研发的安全解决方案,具有良好的可扩展性。该方案还能适应大量的 RPC 的身份验证和加密需求。可以非常方便的在 Google Cloud Platform 云服务上使用。
    • 支持基于令牌的身份验证:gRPC 提供了一种通用机制,可将基于元数据的凭据附加到请求和响应中。对于某些身份验证流,还提供了通过 gRPC 访问 Google API 时获取访问令牌(通常为 OAuth2 令牌)的支持。
  4. 错误码遵循 Google API 错误码规范,这套规范与 HTTP 错误码有对应关系,发生异常时能够快速定位问题。

3 开发示例

3.1 编写 IDL 文件

首先在 src/main 目录下创建 proto 文件夹,并在 src/main/proto 目录下创建 greeter 文件夹,然后将 proto 目录标记为 Sources Root

标记完成后,在 src/main/proto/greeter 文件夹下创建 message.proto 文件,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
syntax = "proto3";

option java_multiple_files = true;
option java_package = "greeter";
option java_outer_classname = "MessageProto";
package greeter;

// 包含用户名的请求消息。
message Request {
    string name = 1;
}

// 包含问候语的响应消息。
message Response {
    string message = 1;
}

然后再在 src/main/proto 文件夹下创建 greeter.proto 文件,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
syntax = "proto3";

option java_multiple_files = true;
option java_package = "greeter";
option java_outer_classname = "GreeterProto";
package greeter;

import "greeter/message.proto";

service Greeter {
    rpc SayHello (Request) returns (Response) {}
}

3.2 Maven 配置

  1. Maven 配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-all</artifactId>
        <version>1.48.1 </version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
  2. 配置 proto 编译插件

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    <build>
        <extensions>
            <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.0</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>
                com.google.protobuf:protoc:3.21.5:exe:${os.detected.classifier}
                </protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                <goals>
                    <goal>compile</goal>
                    <goal>compile-custom</goal>
                </goals>
                </execution>
            </executions>
            </plugin>
        </plugins>
    </build>
  3. 配置输出路径

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <properties>
        <!-- Message 源文件输出目录-->
        <javaOutputDirectory>
            ${project.basedir}/src/main/java-message
        </javaOutputDirectory>
        <!-- gRPC 源文件输出目录-->
        <protocPluginOutputDirectory>
            ${project.basedir}/src/main/java-grpc
        </protocPluginOutputDirectory>
    </properties>

3.3 编译 proto 文件

执行 mvn clean compile 命令完成编译,工程下就生成了 src/main/java-messagesrc/main/java-grpc 文件夹,然后将这两个文件夹标记为 Sources Root

grpc 编译后目录

由图可知,Proto 编译插件根据 Proto 配置文件自动生成了代码。

3.4 实现具体服务

上文示例中,我们通过 Proto 编译插件自动生成了 GreeterGrpc 类。该类不仅提供了 gRPC 所需的方法,还提供了一个我们在配置文件中指定的sayHello方法的默认实现。我们可以通过重写这个方法来实现自己的业务逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package greeter;

public class GreeterRpcService extends GreeterGrpc.GreeterImplBase {

    @Override
    public void sayHello(greeter.Request request,
                                 io.grpc.stub.StreamObserver<greeter.Response> responseObserver) {
        String name = request.getName();
        Response response = Response.newBuilder()
                .setMessage("Hello " + name + "!")
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

3.5 创建 Provider 端

Provider 端需要使用我们重写了业务逻辑的 GreeterRpcService 类,实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import greeter.GreeterRpcService;
import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.io.IOException;

public class GrpcProvider {
    private final Server server;
    
    public GrpcProvider(int port) {
        server = ServerBuilder.forPort(port)
                .addService(new GreeterRpcService())
                .build();
    }
    
    public void start() throws IOException {
        server.start();
    }
    
    public void shutdown() {
        server.shutdown();
    }
}

3.6 创建 Consumer 端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import greeter.GreeterGrpc;
import greeter.Request;
import greeter.Response;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class GrpcConsumer {
    private final GreeterGrpc.GreeterBlockingStub blockingStub;
    
    public GrpcConsumer(String host, int port) {
        ManagedChannel managedChannel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build();
        blockingStub = GreeterGrpc.newBlockingStub(managedChannel);
    }
    
    public String sayHello(String name) {
        Request greeting = Request.newBuilder()
                .setName(name)
                .build();
        Response response = blockingStub.sayHello(greeting);
        return response.getMessage();
    }
}

3.7 执行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.io.IOException;

public class DemoApplication {
    public static void main(String[] args) throws IOException {
        int port = 1234;
        GrpcProvider grpcProvider = new GrpcProvider(port);
        grpcProvider.start();

        GrpcConsumer grpcConsumer = new GrpcConsumer("127.0.0.1", port);
        String reply = grpcConsumer.sayHello("maling");
        System.out.println(reply);
        grpcProvider.shutdown();
    }
}

执行 main 方法后,返回结果:Hello maling!,说明我们成功的完成了一次 gRPC 调用。

4 ProtoBuf

ProtoBuf 的全称是 Google Protocol Buffer,是 Google 公司开发的一种混合语言数据标准。它是一种轻量高效的结构化数据存储格式,可用于序列化。ProtoBuf 具有快速的解析速度,序列化和反序列化的 API 非常简单,文档也非常丰富,并且可以与各种传输层协议结合使用。它还具有 IDL 的设计,并提供了 IDL 的编译器,支持多种编程语言,如 C++、C#、Dart、Golang、Java、Python、Rust 等。

IDL 与异构语言序列化

IDL 是一种异构语言序列化方案。异构语言指的是不同的编程语言。在序列化技术中,它可以被理解为应用程序是用 Java 开发的,但是某个序列化框架并不是针对 Java 的语法设计的,可能是针对 Golang 设计的。此外,该序列化框架采用二进制的数据格式。因此,该序列化框架无法为 Java 开发的应用程序提供序列化和反序列化能力,导致在解析二进制数据时无法正确解析内容。因此,只支持单一语言的序列化框架的应用市场减少了很多。

对异构语言的支持在数据传输中尤为重要,特别涉及到异构语言应用之间的 RPC 调用。目前有两种方案可以解决异构语言问题:

  1. 第一种方案是利用相同的机制重新实现对应语言的序列化框架。

    相同的机制指的是对数据的编排必须保持一致,例如 Kryo 只支持 Java,现在需要支持 Golang 的序列化,以便 Golang 版本的 Kryo 框架能够反序列化 Java 版本的 Kryo 框架序列化的数据(该版本不存在)。在 Golang 版本的 Kryo 框架中,需要遵循 Java 版本的 Kryo 框架的设计原则,才能正确地将 Java 的数据转化为 Golang 的数据,例如将 Java 中的 int 类型数据转化为 Golang 中的 int32 类型数据。目前市面上也有一些开源框架为了满足异构语言的需求而衍生出许多语言的版本,Hessian 就是非常典型的例子:

    Hession 目前已经支持 Java、Python、C++、C#、Erlang、PHP、Ruby、C 等多种语司。它也是一个比较高效的序列化框架,它的实现机制比较注重数据的简化。Hessian 会简化数据类型的元数据表达,对于复杂对象,通过 Java 的反射机制,把对象所有的属性当成一个 Map 来序列化。阿里巴巴在 Hession 的基础上开源了 Hession-lite 版本,解决了一些原版 Hession 存在的问题,并且提升了一些性能,该版本目前已经捐献给 Apache。

  2. 第二种方案就是通过与编程语言无关的 IDL 来解决异构语言的问题,这种方案目前较为常见的有 ThriftProtoBuf

4.1 ProtoBuf 的特性

ProtoBuf 除了支持的语言种类多,在性能上也具有非常大的优势,主要体现在以下三个方面:

  • 用标识符来识别字段

    我们知道 JSON 这种 key-value 的数据格式增加了很多额外的字符,比如 [ 等。在上文的开发示例中可以看到 string name=1 这样的定义,后面的 1 并不是初始值,而是该属性的标识符。

    ProtoBuf 就是用该标识符代替了属性的定义,它被用来在消息的二进制格式中识别各个字段,所以这里的标识符必须是唯一的。序列化时会将该编号及该属性的值一起转化为二进制数据,反序列化时通过标识符就知道该 value 是哪个属性的。这样做的好处是序列化后的数据包大大减小了。

  • 自定义可变数据类型 Varint 用来存储整数

    int 数类型在计算机中占用 4 字节,但是绝大部分的整数都是比较小的整数,实际用不到 4 字节。ProtoBuf 中定义了 Varint 这种数据类型,可以以不同的长度来存储整数,将数据进步进行了压缩,减少了序列化后数据包的大小。

  • 记录字符串长度,解析时直接截取

    上面讲到用标识符来表示字段,后面紧跟着数据,这样可以直接解析数据。如果是字符串类型,则不能直接解析。所以 ProtoBuf 在真实数据前还添加了该字符串的长度,也用 Varint 类型表示。这种策略可以保证反序列化时直接通过字符串长度来截取后面的真实数据 value。

4.2 ProtoBuf 文件格式详解

假设有以下 .proto 文件:

1
2
3
4
5
6
7
8
9
syntax = "proto3";
option java_multiple_files = true;
option java_package = "message";
option java_outer_classname = "Messageproto";
package message;
message Person {
    string name =1;
    int32 age= 2;
}
  • 第一行说明使用的是 ProtoBuf 哪个版本的语法,这里使用的是 proto3 的语法,syntax 语句必须是 .proto 文件除注释和空行外的首行。
  • 第二到第四行是 ProtoBuf 的一些可选配置项:
    • java_multiple_files 设置为 true 代表将编译完成的文件分成多个;
    • java_package 表示编译完后的包名称;
    • java_outer_classname 表示产生的类的类名,编译完成后,会产生一个 MessageProto.java 文件。
  • 第五行定义了类所在的包名,它是一个默认值,当在 *.proto 文件中提供了一个明确的 java_package 配置时,以 java_package 的配置优先。
  • 第六行则是真正的类定义,一个结构化的数据被称为一个 message,在 Java 中一个类就是一个 message,上面的示例中定义了一个 Person 类。
  • 第七行定义了 Person 类中的 name 属性,类型是字符串,标识符为 1。
  • 第八行定义了 Person 类中的 age 属性,类型是 32 位的整型,标识符为 2。

4.3 ProtoBuf 数据类型

ProtoBuf 支持很多语言,比如 C++、Java Python Golang、Ruby、C# 和 PHP 等,并且 ProtoBuf 中的消息类型也对应不同语言中的数据类型,比如上述代码中的 int32 在 Java 中对应的就是 int 类型、uint64 对应的就是 long 类型,部分示例如下:

proto3 TypeC++ TypeJava/Kotlin TypePython TypeGo Type
doubledoubledoublefloatfloat64
floatfloatfloatfloatfloat32
int32int32intintint32
int64int64longint/longint64
uint32uint32intint/longuint32
uint64uint64longint/longuint64

详情可以参考 ProtoBuf 官网,可以看到 ProtoBuf 支持非常多的语言类型,这也是它应用广泛的一个原因。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容