gRPC-Java 使用入门

1. 简介

在此 Codelab 中,您将使用 gRPC-Java 创建一个客户端和一个服务器,它们将构成一个用 Java 编写的路线映射应用的基础。

在本教程结束时,您将拥有一个使用 gRPC 连接到远程服务器的客户端,以获取地图上特定坐标处的位置名称或邮寄地址。一个功能完善的应用可能会使用这种客户端-服务器设计来枚举或总结路线沿途的兴趣点。

服务器的 API 在 Protocol Buffers 文件中定义,该文件将用于为客户端和服务器生成样板代码,以便它们可以相互通信,从而节省您实现该功能的时间和精力。

生成的代码不仅能处理服务器与客户端之间复杂的通信,还能处理数据序列化和反序列化。

学习内容

  • 如何使用 Protocol Buffers 定义服务 API。
  • 如何使用自动代码生成功能基于 Protocol Buffers 定义构建基于 gRPC 的客户端和服务器。
  • 了解使用 gRPC 进行客户端-服务器通信。

此 Codelab 适合刚开始接触 gRPC 或希望复习 gRPC 的 Java 开发者,也适合任何对构建分布式系统感兴趣的人员。无需具备 gRPC 经验。

2. 准备工作

前提条件

  • JDK 版本 8 或更高版本

获取代码

为了避免您完全从头开始,本 Codelab 提供了一个应用源代码框架供您完成。以下步骤将展示如何完成应用,包括使用 Protocol Buffer 编译器插件生成样板 gRPC 代码。

首先,创建 Codelab 工作目录并进入该目录:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

下载并解压缩 Codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

或者,您也可以下载仅包含 Codelab 目录的 .zip 文件,然后手动将其解压缩。

如果您想跳过输入实现代码的步骤,可以在 GitHub 上找到完整的源代码

3. 定义服务

第一步是使用协议缓冲区定义应用的 gRPC 服务、RPC 方法以及请求和响应消息类型。您的服务将提供:

  • 一种名为 GetFeature 的 RPC 方法,由服务器实现并由客户端调用。
  • 使用 GetFeature 方法时,客户端和服务器之间交换的数据结构消息类型 PointFeature。客户端在其 GetFeature 请求中向服务器提供地图坐标作为 Point,服务器则会回复相应的 Feature,其中描述了位于这些坐标处的任何内容。

此 RPC 方法及其消息类型都将在所提供源代码的 src/main/proto/routeguide/route_guide.proto 文件中定义。

协议缓冲区通常称为 protobuf。如需详细了解 gRPC 术语,请参阅 gRPC 的核心概念、架构和生命周期

由于我们在此示例中生成的是 Java 代码,因此我们在 .proto 中指定了 java_package 文件选项和 Java 类的名称:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

消息类型

在源代码的 routeguide/route_guide.proto 文件中,首先定义 Point 消息类型。Point 表示地图上的纬度-经度坐标对。在此 Codelab 中,请使用整数作为坐标:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

数字 12message 结构中每个字段的唯一 ID 编号。

接下来,定义 Feature 消息类型。Feature 使用 string 字段来表示由 Point 指定的位置处的某个事物的名称或邮政地址:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

服务方法

route_guide.proto 文件具有名为 RouteGuideservice 结构,用于定义应用服务提供的一个或多个方法。

RouteGuide 定义中添加 rpc 方法 GetFeature。如前所述,此方法将根据给定的坐标集查找某个位置的名称或地址,因此让 GetFeature 为给定的 Point 返回 Feature

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

这是一元 RPC 方法:一种简单 RPC,其中客户端向服务器发送请求并等待响应返回,就像本地函数调用一样。

4. 生成客户端和服务器代码

接下来,我们需要根据 .proto 服务定义生成 gRPC 客户端和服务器接口。我们使用协议缓冲区编译器 protoc 和一个特殊的 gRPC Java 插件来完成此操作。您需要使用 proto3 编译器(同时支持 proto2 和 proto3 语法)才能生成 gRPC 服务。

使用 Gradle 或 Maven 时,protoc 构建插件可以在构建过程中生成必要的代码。您可以参阅 grpc-java README,了解如何从您自己的 .proto 文件生成代码。

我们在 Codelab 的源代码中提供了 Gradle 环境和配置,用于构建此项目。

grpc-java-getting-started 目录中,运行以下命令:

$ chmod +x gradlew
$ ./gradlew generateProto

以下类是从我们的服务定义生成的:

  • Feature.javaPoint.java 和其他包含所有协议缓冲区代码的文件,用于填充、序列化和检索我们的请求和响应消息类型。
  • RouteGuideGrpc.java,其中包含(以及一些其他实用代码)供 RouteGuide 服务器实现的基类 RouteGuideGrpc.RouteGuideImplBase,以及 RouteGuide 服务和存根类中定义的所有方法,供客户端使用。

5. 实现服务器

首先,我们来看看如何创建 RouteGuide 服务器。要让 RouteGuide 服务正常运行,需要完成以下两个部分:

  • 实现从服务定义生成的服务接口,该接口负责服务的实际“工作”。
  • 运行 gRPC 服务器以监听来自客户端的请求,并将这些请求分派给正确的服务实现。

实现 RouteGuide

如您所见,我们的服务器有一个 RouteGuideService 类,用于扩展生成的 RouteGuideGrpc.RouteGuideImplBase 抽象类:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

我们提供了以下 2 个文件,用于初始化具有功能的服务器:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

下面我们来详细了解一下简单的 RPC 实现。

一元 RPC

RouteGuideService 实现所有服务方法。在本例中,它只是 GetFeature(),从客户端获取 Point 消息,并以 Feature 消息的形式从已知地点的列表中返回相应的位置信息。

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

getFeature() 方法接受两个参数:

  • Point:请求。
  • StreamObserver<Feature>:一个响应观测器,这是服务器用来调用其响应的特殊接口。

如需将我们的回答返回给客户端并完成通话,请执行以下操作:

  1. 我们根据服务定义构建并填充 Feature 响应对象,以返回给客户端。在此示例中,我们在单独的私有 checkFeature() 方法中执行此操作。
  2. 我们使用响应观察器的 onNext() 方法返回 Feature
  3. 我们使用响应观察器的 onCompleted() 方法来指定我们已完成 RPC 处理。

启动服务器

实现所有服务方法后,我们需要启动 gRPC 服务器,以便客户端能够实际使用我们的服务。我们在样板中加入了 ServerBuilder 对象的创建:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

我们在构造函数中构建服务:

  1. 使用构建器的 forPort() 方法指定我们想要用于侦听客户端请求的端口(它将使用通配符地址)。
  2. 创建服务实现类 RouteGuideService 的实例,并将其传递给构建器的 addService() 方法。
  3. 在构建器上调用 build(),为我们的服务创建 RPC 服务器。

以下代码段展示了如何创建 ServerBuilder 对象。

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

以下代码段展示了如何为 RouteGuide 服务创建服务器对象。

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

实现一个启动方法,该方法在上面创建的服务器上调用 start

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

实现一种等待服务器完成的方法,以便服务器不会立即退出。

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

如您所见,我们使用 ServerBuilder 构建并启动服务器。

在 main 方法中,我们执行以下操作:

  1. 创建 RouteGuideServer 实例。
  2. 调用 start() 可为我们的服务激活 RPC 服务器。
  3. 通过调用 blockUntilShutdown() 等待服务停止。
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. 创建客户端

在本部分中,我们将了解如何为 RouteGuide 服务创建客户端。

实例化桩

如需调用服务方法,我们首先需要创建 stub。桩有两种类型,但在此 Codelab 中,我们只需要使用阻塞型桩。这两种类型如下:

  • 一个阻塞/同步桩,用于发出 RPC 调用并等待服务器响应,然后返回响应或引发异常。
  • 一个非阻塞/异步桩,用于向服务器发出非阻塞调用,其中响应以异步方式返回。您只能通过使用异步 stub 来进行某些类型的流式调用。

首先,我们需要创建一个 gRPC 渠道,然后使用该渠道创建桩。

我们可以直接使用 ManagedChannelBuilder 来创建渠道。

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

不过,我们来使用一个采用包含 hostname:port 的字符串的实用方法。

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

现在,我们可以使用该渠道创建阻塞桩。在此 Codelab 中,我们只有阻塞 RPC,因此我们使用从 .proto 生成的 RouteGuideGrpc 类中提供的 newBlockingStub 方法。

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

调用服务方法

现在,我们来看看如何调用服务方法。

简单 RPC

调用简单的 RPC GetFeature 几乎与调用本地方法一样简单。

我们创建并填充请求协议缓冲区对象(在本例中为 Point),将其传递给阻塞 stub 上的 getFeature() 方法,然后获得 Feature

如果发生错误,系统会将其编码为 Status,我们可以从 StatusRuntimeException 中获取该错误。

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

样板会记录一条消息,其中包含的内容取决于指定点是否存在功能。

7. 试试看!

  1. start_here 目录中,运行以下命令:
$ ./gradlew installDist

此命令将编译您的代码,将其打包到 JAR 中,并创建运行示例的脚本。它们将在 build/install/start_here/bin/ 目录中创建。脚本为:route-guide-serverroute-guide-client

在启动客户端之前,服务器需要处于运行状态。

  1. 运行服务器:
$ ./build/install/start_here/bin/route-guide-server
  1. 运行客户端:
$ ./build/install/start_here/bin/route-guide-client

您将看到如下输出(为清晰起见,省略了时间戳):

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. 后续步骤