1. 简介

gRPC是一个高性能、开源的 RPC 框架,最初由 Google 开发。 它有助于消除样板代码,并有助于在数据中心内和跨数据中心连接多语言服务。

2. 概述

gRPC框架基于远程过程调用的客户端-服务器模型。客户端应用程序可以直接调用服务器应用程序上的方法,就像它是本地对象一样。

本文将使用以下步骤使用 gRPC 创建一个典型的客户端 - 服务器应用程序:

  1. 在*.proto*文件中定义服务
  2. 使用protocol buffer 编译器生成服务器和客户端代码
  3. 创建服务器应用程序,实现生成的服务接口并生成 gRPC 服务器
  4. 创建客户端应用程序,使用生成的stubs进行 RPC 调用

让我们定义一个简单的HelloService,它返回一句问候语。

3. Maven 依赖

让我们添加grpc-nettygrpc-protobuf、和grpc-stub依赖项:

<properties>
  <grpc.version>1.42.1</grpc.version>
</properties>


<dependencies>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty</artifactId>
    <version>${grpc.version}</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>${grpc.version}</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>${grpc.version}</version>
  </dependency>
</dependencies>

4. 定义服务

我们首先定义一个服务,指定可以远程调用的方法及其参数和返回类型

这是在*.proto*文件中定义。它是用于描述消息的结构。

4.1. 基本配置

让我们为示例HelloService创建一个HelloService.proto文件。我们首先添加一些基本的配置细节:

syntax = "proto3";
option java_multiple_files = true;
package com.ripjava.grpc;

第一行告诉编译器在这个文件中使用了什么语法。默认情况下,编译器在单个 Java 文件中生成所有 Java 代码。

第二行覆盖上面的设置,所有内容都将在单个文件中生成。

最后,我们指定要用于生成的 Java 类的包。

4.2. 定义消息结构

接下来,我们定义消息:

message HelloRequest {
    string firstName = 1;
    string lastName = 2;
}

这定义了一个请求消息。消息的每个属性都与其类型一起定义。

需要为每个属性分配一个唯一编号,称为标记。protobuf使用此标记来表示属性,而不是使用属性名称。

因此,与我们每次都传递属性名称firstName 的JSON 不同,协议缓冲区将使用数字 1 来表示firstName

响应负消息的定义类似于请求。

请注意,我们可以在多种消息类型中使用相同的标签:

message HelloResponse {
    string greeting = 1;
}

4.3. 定义服务接口

最后,让我们定义服务接口。对于我们的*HelloService,我们定义了一个hello()*方法:

service HelloService {
    rpc hello(HelloRequest) returns (HelloResponse);
}

hello() 操作接受一个请求参数,并返回一个响应消息。

gRPC 还通过在请求和响应前添加stream关键字来支持流式传输。

5. 生成代码

现在我们将HelloService.proto文件传递给协议缓冲区编译器protoc以生成 Java 文件。有多种方法可以生产代码。

5.1. 使用protobuf编译器

首先,我们需要协议缓冲区编译器。我们可以从这里提供的许多预编译二进制文件中进行选择。

此外,我们需要获取gRPC Java Codegen Plugin

最后,我们可以使用以下命令来生成代码:

protoc --plugin=protoc-gen-grpc-java=$PATH_TO_PLUGIN -I=$SRC_DIR 
  --java_out=$DST_DIR --grpc-java_out=$DST_DIR $SRC_DIR/HelloService.proto

5.2. 使用 Maven 插件

作为开发人员,您希望代码生成与您的构建系统紧密集成。gRPC 为Maven 构建系统提供了一个protobuf-maven-plugin

<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.3.0:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>
          io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}
        </pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

OS-Maven的插件会帮我们生成各种有用的平台相关的项目属性,比如$ {os.detected.classifier}

6. 创建服务器

无论您使用哪种方法生成代码,都会生成以下关键文件:

  • HelloRequest.java :包含HelloRequest类型定义
  • HelloResponse.java :这包含HelleResponse类型定义
  • HelloServiceImplBase.java :它包含抽象类HelloServiceImplBase,它提供了我们在服务接口中定义的所有操作的实现

6.1. 覆盖服务基类

抽象类HelloServiceImplBase 的默认实现是抛出运行时异常io.grpc.StatusRuntimeException表示该方法未实现。

我们将扩展这个类并覆盖我们服务定义中提到的*hello()*方法:

public class HelloServiceImpl extends HelloServiceImplBase {

    @Override
    public void hello(
      HelloRequest request, StreamObserver<HelloResponse> responseObserver) {

        String greeting = new StringBuilder()
          .append("Hello, ")
          .append(request.getFirstName())
          .append(" ")
          .append(request.getLastName())
          .toString();

        HelloResponse response = HelloResponse.newBuilder()
          .setGreeting(greeting)
          .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

如果我们将hello()的签名与我们在HellService.proto文件中编写的签名进行比较,我们会注意到它没有返回HelloResponse。相反,它将第二个参数作为StreamObserver,它是一个响应观察者,一个服务器的回调,以调用它的响应。

这样客户端就可以选择进行阻塞调用或非阻塞调用。

gRPC 使用构建器来创建对象。我们使用HelloResponse.newBuilder()并设置问候文本来构建一个HelloResponse对象。我们将此对象设置为 responseObserver 的*onNext()*方法以将其发送给客户端。

最后,我们需要调用*onCompleted()*来指定我们已经完成了 RPC 的处理,否则连接将被挂起,客户端将等待更多的信息进来。

6.2. 启动Grpc 服务器

接下来,我们需要启动 gRPC 服务器来监听传入的请求:

public class GrpcServer {
    public static void main(String[] args) throws Exception {
        Server server = ServerBuilder
                .forPort(8080)
                .addService(new HelloServiceImpl()).build();

        server.start();
        server.awaitTermination();
    }
}

在这里,我们再次使用构建器在端口 8080 上创建 gRPC 服务器并添加我们定义的HelloServiceImpl服务。

*start()*将启动服务器。

在我们的示例中,我们将调用*awaitTermination()*以保持服务器在前台运行以阻止提示。

7. 创建客户端

**gRPC 提供了一个通道构造,可以抽象出底层细节,**如连接、连接池、负载平衡等。

我们将使用ManagedChannelBuilder创建一个频道。在这里,我们指定服务器地址和端口。

我们将使用没有任何加密的纯文本:

public class GrpcClient {
    public static void main(String[] args) {

        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
                .usePlaintext()
                .build();

        HelloServiceGrpc.HelloServiceBlockingStub stub
                = HelloServiceGrpc.newBlockingStub(channel);

        HelloResponse helloResponse = stub.hello(
                HelloRequest.newBuilder()
                .setFirstName("ripjava.com")
                .setLastName("gRPC")
                .build());

        System.out.println(helloResponse.getGreeting());
        
        channel.shutdown();
    }
}

接下来,我们需要创建一个stub,我们将使用它来对*hello()*进行实际的远程调用。

**stub是客户端与服务器交互的主要方式。**使用自动生成的stub时,stub类将具有用于包装通道的构造函数。

这里我们使用了阻塞/同步stub,以便 RPC 调用等待服务器响应,并且将返回响应或引发异常。

gRPC 提供了另外两种类型的stub,它们有助于非阻塞/异步调用。

最后,是时候进行hello() RPC 调用了。这里我们传递了HelloRequest

我们可以使用自动生成的 setter 来设置HelloRequest对象的firstNamelastName属性。

最后,服务器返回HelloResponse对象。

启动服务器之后,在启动客户端,客户端输出如下:

Hello, ripjava.com gRPC

8. 结论

在本文中,我们看到了如何使用 gRPC 通过专注于定义服务并让 gRPC 处理所有样板代码来简化两个服务之间通信的开发。

像往常一样,您可以在 GitHub 上找到源代码。