使用 Java 构建 gRPC 服务

1. 概览

gRPC 是 Google 开发的语言中立、平台中立的远程过程调用 (RPC) 框架和工具集。借助它,您可以使用 Protocol Buffers(一种功能强大的二进制序列化工具集和语言)来定义服务。然后,您可以根据服务定义生成各种语言的惯用客户端和服务器桩。

在此 Codelab 中,您将学习如何构建一个基于 Java 的服务,该服务使用 gRPC 框架公开 API,然后编写一个客户端来使用生成的 gRPC 客户端桩。

学习内容

  • Protocol Buffer 语言
  • 如何使用 Java 实现 gRPC 服务
  • 如何使用 Java 实现 gRPC 客户端

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价自己在构建 Node.js 应用方面的经验水平?

新手水平 中等水平 熟练水平

您如何评价自己在构建 Go 应用方面的经验水平?

新手水平 中等水平 熟练水平

2. 设置和要求

自定进度的环境设置

  1. 登录 Cloud 控制台,然后创建一个新项目或重复使用现有项目。 如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID

  1. 接下来,您需要在 Cloud 控制台中启用结算功能,才能使用 Google Cloud 资源。

运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照“清理”部分中的所有说明操作,该部分介绍了如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

Google Cloud Shell

虽然您可以在计算机上操作此 Codelab,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

基于 Debian 的这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。这意味着在本 Codelab 中,您只需要一个浏览器(没错,它适用于 Chromebook)。

  1. 如需从 Cloud Console 激活 Cloud Shell,只需点击激活 Cloud Shella8460e837e9f5fda.png(预配和连接到环境仅需花费一些时间)。

b532b2f19ab85dda.png

Screen Shot 2017-06-14 at 10.13.43 PM.png

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的 PROJECT_ID

gcloud auth list

命令输出

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果出于某种原因未设置项目,只需发出以下命令即可:

gcloud config set project <PROJECT_ID>

正在查找您的 PROJECT_ID?检查您在设置步骤中使用的 ID,或在 Cloud Console 信息中心查找该 ID:

2485e00c1223af09.png

默认情况下,Cloud Shell 还会设置一些环境变量,这对您日后运行命令可能会很有用。

echo $GOOGLE_CLOUD_PROJECT

命令输出

<PROJECT_ID>
  1. 最后,设置默认可用区和项目配置。
gcloud config set compute/zone us-central1-f

您可以选择各种不同的可用区。如需了解详情,请参阅区域和可用区

3. 创建 gRPC 服务

使用 Maven 创建新的 Java 项目:

$ mvn archetype:generate -DgroupId=com.example.grpc \
 -DartifactId=grpc-hello-server \
 -DarchetypeArtifactId=maven-archetype-quickstart \
 -DinteractiveMode=false
$ cd grpc-hello-server

添加 gRPC 定义文件

在 gRPC 中,服务载荷(请求和响应)和服务操作需要捕获在 IDL(接口定义语言)中。gRPC 使用 Protobuffer 3 语法来定义消息载荷和操作。我们来创建一个简单的 Greeting 服务的 proto 文件,其中包含 Hello 请求和 Hello 响应。

首先,创建一个新的 proto 目录来保存新的 proto 文件:

$ mkdir -p src/main/proto

然后,创建一个新的 proto 文件 src/main/proto/GreetingService.proto

您可以使用 vim,nano,emacs 编辑文件:

src/main/proto/GreetingService.proto

syntax = "proto3";
package com.example.grpc;

// Request payload
message HelloRequest {
  // Each message attribute is strongly typed.
  // You also must assign a "tag" number.
  // Each tag number is unique within the message.
  string name = 1;

  // This defines a strongly typed list of String
  repeated string hobbies = 2;

  // There are many more basics types, like Enum, Map
  // See https://developers.google.com/protocol-buffers/docs/proto3
  // for more information.
}

message HelloResponse {
  string greeting = 1;
}

// Defining a Service, a Service can have multiple RPC operations
service GreetingService {
  // Define a RPC operation
  rpc greeting(HelloRequest) returns (HelloResponse);
}

添加 gRPC 依赖项和插件

有了定义后,我们就可以根据此文件生成服务器端桩和客户端桩。您需要添加 gRPC 依赖项和插件。

首先,将 gRPC 依赖项添加到 pom.xml

pom.xml

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-netty-shaded</artifactId>
      <version>1.24.0</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-protobuf</artifactId>
      <version>1.24.0</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-stub</artifactId>
      <version>1.24.0</version>
    </dependency>
    <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
    </dependency>
    ...
  </dependencies>
  ...
</project>

然后添加插件:

pom.xml

<project>
  ...
  <dependencies>
    ...
  </dependencies>
  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.6.2</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.9.0:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.24.0:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

生成桩

构建应用时,插件会将 proto 定义转换为 Java 代码。

$ mvn -DskipTests package

如需查看生成的文件,请执行以下操作:

$ find target/generated-sources

实现服务

首先,创建一个新的 GreetingServiceImpl 类,该类将实现 greeting 操作:

src/main/java/com/example/grpc/GreetingServiceImpl.java

package com.example.grpc;

import io.grpc.stub.StreamObserver;

public class GreetingServiceImpl extends GreetingServiceGrpc.GreetingServiceImplBase {
  @Override
  public void greeting(GreetingServiceOuterClass.HelloRequest request,
        StreamObserver<GreetingServiceOuterClass.HelloResponse> responseObserver) {
  // HelloRequest has toString auto-generated.
    System.out.println(request);

    // You must use a builder to construct a new Protobuffer object
    GreetingServiceOuterClass.HelloResponse response = GreetingServiceOuterClass.HelloResponse.newBuilder()
      .setGreeting("Hello there, " + request.getName())
      .build();

    // Use responseObserver to send a single response back
    responseObserver.onNext(response);

    // When you are done, you must call onCompleted.
    responseObserver.onCompleted();
  }
}

实现服务器

最后,您需要启动一个服务器来监听端口并注册此服务实现。修改 App 类及其 main 方法:

src/main/java/com/example/grpc/App.java

package com.example.grpc;

import io.grpc.*;

public class App
{
    public static void main( String[] args ) throws Exception
    {
      // Create a new server to listen on port 8080
      Server server = ServerBuilder.forPort(8080)
        .addService(new GreetingServiceImpl())
        .build();

      // Start the server
      server.start();

      // Server threads are running in the background.
      System.out.println("Server started");
      // Don't exit the main thread. Wait until server is terminated.
      server.awaitTermination();
    }
}

最后,运行服务器:

$ mvn -DskipTests package exec:java -Dexec.mainClass=com.example.grpc.App
...
Server Started

4. 使用服务

生成器已生成所有客户端桩。为简化实验,我们将使用相同的 Maven 项目,但只需添加一个包含新 main 方法的新 Client 类。

首先,点击 + 打开新的 Cloud Shell 会话,这样您就不需要终止服务器:

1ff0fda960b9adfc.png

在新会话中,切换到 grpc-hello-server 目录:

$ cd grpc-hello-server

然后,添加新的 Client 类:

src/main/java/com/example/grpc/Client.java

package com.example.grpc;

import io.grpc.*;

public class Client
{
    public static void main( String[] args ) throws Exception
    {
      // Channel is the abstraction to connect to a service endpoint
      // Let's use plaintext communication because we don't have certs
      final ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:8080")
        .usePlaintext(true)
        .build();

      // It is up to the client to determine whether to block the call
      // Here we create a blocking stub, but an async stub,
      // or an async stub with Future are always possible.
      GreetingServiceGrpc.GreetingServiceBlockingStub stub = GreetingServiceGrpc.newBlockingStub(channel);
      GreetingServiceOuterClass.HelloRequest request =
        GreetingServiceOuterClass.HelloRequest.newBuilder()
          .setName("Ray")
          .build();

      // Finally, make the call using the stub
      GreetingServiceOuterClass.HelloResponse response = 
        stub.greeting(request);

      System.out.println(response);

      // A Channel should be shutdown before stopping the process.
      channel.shutdownNow();
    }
}

最后,运行客户端:

$ mvn -DskipTests package exec:java -Dexec.mainClass=com.example.grpc.Client
...
greeting: "Hello there, Ray"

大功告成!很简单,对吧?

5. 在线媒体服务

您还可以尝试更多操作。例如,您只需在 proto 文件中向请求或响应参数添加 stream 关键字,即可轻松构建流式传输服务,例如:

src/main/proto/GreetingService.proto

syntax = "proto3";
package com.example.grpc;

...

// Defining a Service, a Service can have multiple RPC operations
service GreetingService {
  // MODIFY HERE: Update the return to streaming return.
  rpc greeting(HelloRequest) returns (stream HelloResponse);
}

更新服务器,使其发送多个响应,而不仅仅是一个。您可以通过多次调用 responseObserver.onNext(...) 来实现此目的:

src/main/java/com/example/grpc/GreetingServiceImpl.java

package com.example.grpc;

import io.grpc.stub.StreamObserver;

public class GreetingServiceImpl extends GreetingServiceGrpc.GreetingServiceImplBase {
  @Override
  public void greeting(GreetingServiceOuterClass.HelloRequest request,
        StreamObserver<GreetingServiceOuterClass.HelloResponse> responseObserver) {
 
    ...

    // Feel free to construct different responses if you'd like.
    responseObserver.onNext(response);
    responseObserver.onNext(response);
    responseObserver.onNext(response);

    // When you are done, you must call onCompleted.
    responseObserver.onCompleted();
  }
}

客户端必须使用异步 stub,而不是阻塞 stub。更新客户端代码,确保将 stub 类型更新为 GreetingServiceStub

src/main/java/com/example/grpc/Client.java

package com.example.grpc;

import io.grpc.*;

// New import
import io.grpc.stub.*;

public class Client
{
    public static void main( String[] args ) throws Exception
    {
      final ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:8080")
        .usePlaintext(true)
        .build();

      // Replace the previous synchronous code with asynchronous code.
      // This time use an async stub:
       GreetingServiceGrpc.GreetingServiceStub stub = GreetingServiceGrpc.newStub(channel);

      // Construct a request
      GreetingServiceOuterClass.HelloRequest request =
        GreetingServiceOuterClass.HelloRequest.newBuilder()
          .setName("Ray")
          .build();

      // Make an Asynchronous call. Listen to responses w/ StreamObserver
      stub.greeting(request, new StreamObserver<GreetingServiceOuterClass.HelloResponse>() {
        public void onNext(GreetingServiceOuterClass.HelloResponse response) {
          System.out.println(response);
        }
        public void onError(Throwable t) {
        }
        public void onCompleted() {
          // Typically you'll shutdown the channel somewhere else.
          // But for the purpose of the lab, we are only making a single
          // request. We'll shutdown as soon as this request is done.
          channel.shutdownNow();
        }
      });
    }
}

重新构建应用:

$ mvn -DskipTests package

在各自的 Cloud Shell 会话中重启服务器和客户端。

如需启动服务器,请执行以下操作:

$ mvn exec:java -Dexec.mainClass=com.example.grpc.App
...
Server Started

如需启动客户端,请执行以下操作:

$ mvn exec:java -Dexec.mainClass=com.example.grpc.Client
...
greeting: "Hello there, Ray"
greeting: "Hello there, Ray"
greeting: "Hello there, Ray"

6. 恭喜!

所学内容

  • Protocol Buffer 语言
  • 如何使用 Java 实现 gRPC 服务器
  • 如何使用 Java 实现 gRPC 客户端

后续步骤

向我们提供反馈

  • 请抽出片刻时间,完成这份非常简短的调查问卷