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
方法时,客户端和服务器之间交换的数据结构消息类型Point
和Feature
。客户端在其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;
}
数字 1
和 2
是 message
结构中每个字段的唯一 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
文件具有名为 RouteGuide
的 service
结构,用于定义应用服务提供的一个或多个方法。
在 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.java
、Point.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>
:一个响应观测器,这是服务器用来调用其响应的特殊接口。
如需将我们的回答返回给客户端并完成通话,请执行以下操作:
- 我们根据服务定义构建并填充
Feature
响应对象,以返回给客户端。在此示例中,我们在单独的私有checkFeature()
方法中执行此操作。 - 我们使用响应观察器的
onNext()
方法返回Feature
。 - 我们使用响应观察器的
onCompleted()
方法来指定我们已完成 RPC 处理。
启动服务器
实现所有服务方法后,我们需要启动 gRPC 服务器,以便客户端能够实际使用我们的服务。我们在样板中加入了 ServerBuilder 对象的创建:
ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)
我们在构造函数中构建服务:
- 使用构建器的
forPort()
方法指定我们想要用于侦听客户端请求的端口(它将使用通配符地址)。 - 创建服务实现类
RouteGuideService
的实例,并将其传递给构建器的addService()
方法。 - 在构建器上调用
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 方法中,我们执行以下操作:
- 创建
RouteGuideServer
实例。 - 调用
start()
可为我们的服务激活 RPC 服务器。 - 通过调用
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. 试试看!
- 在
start_here
目录中,运行以下命令:
$ ./gradlew installDist
此命令将编译您的代码,将其打包到 JAR 中,并创建运行示例的脚本。它们将在 build/install/start_here/bin/
目录中创建。脚本为:route-guide-server
和 route-guide-client
。
在启动客户端之前,服务器需要处于运行状态。
- 运行服务器:
$ ./build/install/start_here/bin/route-guide-server
- 运行客户端:
$ ./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