本教程提供了一个Java程序员使用gRPC的基本介绍。
通过这个例子,您将学习如何:
- 在.proto文件中定义一个服务。
- 使用协议缓冲区编译器生成服务器和客户端代码。
- 使用Java gRPC API为您的服务编写一个简单的客户端和服务器。
它假设您已阅读概述并熟悉协议缓冲区。
请注意,本教程中的示例使用协议缓冲区语言的proto3版本,该版本目前在beta版本中:
您可以在proto3语言指南和Java代码生成指南中找到更多内容,
并在协议缓冲区Github仓库中查看新版本的发行说明。
为什么要使用gRPC?
我们的示例是一个简单的路由映射应用程序,它允许客户端获取有关其路由上的功能的信息,创建其路由的摘要,并与服务器和其他客户端交换路由信息,如流量更新。
使用gRPC,我们可以在.proto
文件中定义一次服务,并以任何gRPC支持的语言实现客户端和服务器,这反过来可以在从Google内部的服务器到您自己的平板电脑的环境中运行,所有不同语言和环境之间的通信的复杂性由gRPC处理。
我们还获得使用协议缓冲区的所有优势,包括高效的序列化,简单的IDL和简单的接口。
示例代码和设置
我们的教程的示例代码是grpc/grpc-java/examples/src/main/java/io/grpc/examples。
要下载该示例,请通过运行以下命令克隆grpc-java存储库中的最新版本:
$ git clone -b v1.2.0 https://github.com/grpc/grpc-java.git
然后改变您当前的目录为grpc-java/examples
:
$ cd grpc-java/examples
定义服务
我们的第一步(从概述中可以看出)是使用协议缓冲区定义gRPC服务和方法请求和响应类型。
您可以在grpc-java/examples/src/main/proto/route_guide.proto中看到完整的.proto文件。
在这个例子中我们要生成Java代码,我们在.proto
中指定了一个java_package
选项:
option java_package = "io.grpc.examples";
这指定了我们要为生成的Java类使用的包。如果在.proto
文件中没有提供明确的java_package
选项,则默认情况下将使用proto
包(使用“package”关键字指定)。建议提供了java_package选项,因为普通的.proto包声明不会以倒置的域名开头。
如果我们从这个.proto
生成另一种语言的代码,那么java_package
选项就没有效果。
我们在.proto
文件中指定一个命名后的服务:
service RouteGuide {
...
}
然后我们在服务定义中定义rpc方法,指定它们的请求和响应类型。gRPC允许您定义四种服务方式,所有这些都在RouteGuide
服务中使用:
- 一个简单的RPC,客户端使用存根向服务器发送请求,并等待响应回来,就像普通的函数调用一样。
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- 服务器端流式RPC,其中客户端向服务器发送请求并获取流以读取一系列消息。客户端读取返回的流,直到没有更多的消息。
在我们的示例中可以看到,您可以通过将stream
关键字放在响应类型之前指定服务器端流方法:
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
您可以通过将stream
关键字放在请求类型之前指定客户端流方法:
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
您可以在请求和响应之前放置stream
关键字来指定此类型的方法:
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
我们的.proto
文件还包含在我们的服务方法中使用的所有请求和响应类型的协议缓冲区消息类型定义 - 例如,这里是Point
消息类型的定义:
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
生成客户端和服务器代码
接下来,我们需要从.proto
服务定义中生成gRPC客户端和服务器接口。我们使用协议缓冲区编译器protoc
与一个特殊的gRPC Java插件。您需要使用proto3编译器(它支持proto2和proto3语法)才能生成gRPC服务。
当使用Gradle
或Maven
时,protoc
build插件可以生成必要的代码作为构建的一部分。您可以参考README,了解如何从自己的.proto
文件生成代码。
从我们的服务定义生成以下类:
Feature.java
,Point.java
,Rectangle.java
, 和其他所有协议缓冲区代码来填充,序列化和检索我们的请求和响应消息类型。RouteGuideGrpc.java
其中包含(以及一些其他有用的代码):
RouteGuide
: 服务器实现的基类,RouteGuideGrpc.RouteGuideImplBase
: 具有RouteGuide
服务中定义的所有方法。- 存根类,客户端可以使用它来与
RouteGuide
服务器通信。
创建服务器
首先我们来看看如何创建一个RouteGuide
服务器。如果您只对创建gRPC客户端感兴趣,可以跳过本节,直接创建客户端(尽管您可能会发现它很有趣)。
我们的RouteGuide
服务有两个部分:
- 覆盖从我们的服务定义中生成的服务基类:做我们服务的实际“工作”。
- 运行gRPC服务器来监听来自客户端的请求并返回服务响应。
您可以在grpc-java/examples/src/main/java/io/grpc/examples/RouteGuideServer.java中找到我们的示例RouteGuide
服务器,
我们来仔细看看它是如何工作的。
实现RouteGuide
如您所见,我们的服务器有一个RouteGuideService
类,用于继承生成的RouteGuideGrpc.RoutGuideImplBase
抽象类:
private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}
简单的RPC
RouteGuideService
实现我们所有的服务方法。让我们先看一下最简单的类型GetFeature
, 它从客户端获取一个Point
,并从数据库中的Feature
返回它相应的功能信息。
@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
responseObserver.onNext(checkFeature(request));
responseObserver.onCompleted();
}
...
private Feature checkFeature(Point location) {
for (Feature feature : features) {
if (feature.getLocation().getLatitude() == location.getLatitude()
&& feature.getLocation().getLongitude() == location.getLongitude()) {
return feature;
}
}
// No feature was found, return an unnamed feature.
return Feature.newBuilder().setName("").setLocation(location).build();
}
getFeature()
有两个参数:
Point
:请求StreamObserver<Feature>
: 一个响应观察者,它是服务器调用其响应的特殊接口。
将我们的响应返回给客户并完成通话:
- 如我们的服务定义中所指定的那样,我们构造并填充
Feature
响应对象以返回到客户端。在这个例子中,我们在一个单独的私有checkFeature()
方法中实现。 - 我们使用响应观察者的
onNext()
方法来返回Feature
。 - 我们使用响应观察者的
onCompleted()
方法来指定我们已经完成了对RPC的处理。
服务器端流式RPC
接下来,我们来看看我们的一个流式RPC。ListFeatures
是服务器端流式RPC,因此我们需要将多个Features
发送给我们的客户端。
private final Collection<Feature> features;
...
@Override
public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
int left = min(request.getLo().getLongitude(), request.getHi().getLongitude());
int right = max(request.getLo().getLongitude(), request.getHi().getLongitude());
int top = max(request.getLo().getLatitude(), request.getHi().getLatitude());
int bottom = min(request.getLo().getLatitude(), request.getHi().getLatitude());
for (Feature feature : features) {
if (!RouteGuideUtil.exists(feature)) {
continue;
}
int lat = feature.getLocation().getLatitude();
int lon = feature.getLocation().getLongitude();
if (lon >= left && lon <= right && lat >= bottom && lat <= top) {
responseObserver.onNext(feature);
}
}
responseObserver.onCompleted();
}
像简单的RPC一样,这个方法获得一个请求对象(客户端期望从Rectangle
找到满足条件的Feature
)和一个StreamObserver
响应观察器。
这次,我们得到了需要返回给客户端的足够多的Feature
对象(在这种场景下,我们从服务的特征集中选择它们是否在我们的请求Rectangle
内),并使用其onNext()
方法将它们依次写入响应观察者。
最后,像我们简单的RPC一样,我们使用响应观察者的onCompleted()
方法告诉gRPC我们已经完成了写入响应。
客户端流式RPC
现在让我们来看看一些更复杂的事情:客户端流方法RecordRoute
,在这里我们从客户端获取一个Points
流,并返回一个包括它们路径的信息RouteSummary
。
@Override
public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {
return new StreamObserver<Point>() {
int pointCount;
int featureCount;
int distance;
Point previous;
long startTime = System.nanoTime();
@Override
public void onNext(Point point) {
pointCount++;
if (RouteGuideUtil.exists(checkFeature(point))) {
featureCount++;
}
// For each point after the first, add the incremental distance from the previous point
// to the total distance value.
if (previous != null) {
distance += calcDistance(previous, point);
}
previous = point;
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "Encountered error in recordRoute", t);
}
@Override
public void onCompleted() {
long seconds = NANOSECONDS.toSeconds(System.nanoTime() - startTime);
responseObserver.onNext(RouteSummary.newBuilder().setPointCount(pointCount)
.setFeatureCount(featureCount).setDistance(distance)
.setElapsedTime((int) seconds).build());
responseObserver.onCompleted();
}
};
}
你可以看到,像之前的方法类型一样,我们的方法得到一个StreamObserver
响应观察器参数,但是这次返回一个StreamObserver
以便客户端写入它的Point
。
在方法体中我们实例化一个匿名的StreamObserver
并返回:
- 覆盖
onNext()
方法,以便每次客户端将Point
写入消息流时,拿到特性和其它信息。 - 覆盖
onCompleted()
方法(当客户端完成写入消息时调用)来填充和构建我们的RouteSummary
。然后我们用RouteSummary
调用方法自己的响应观察者的onNext()
,之后调用它的onCompleted()
方法,结束服务器端的调用。
双向流RPC
最后,让我们看看双向流式RPCRouteChat()
。
@Override
public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {
return new StreamObserver<RouteNote>() {
@Override
public void onNext(RouteNote note) {
List<RouteNote> notes = getOrCreateNotes(note.getLocation());
// Respond with all previous notes at this location.
for (RouteNote prevNote : notes.toArray(new RouteNote[0])) {
responseObserver.onNext(prevNote);
}
// Now add the new note to the list
notes.add(note);
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "Encountered error in routeChat", t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
和我们的客户端流的例子一样,我们拿到和返回一个StreamObserver
响应观察者,除了这次我们在客户端仍然写入消息到它们的消息流时通过我们方法的响应观察者返回值。
这里读写的语法和客户端流以及服务器流方法一样。虽然每一端都会按照它们写入的顺序拿到另一端的消息,客户端和服务器都可以任意顺序读写——流的操作是互不依赖的。
启动服务器
一旦我们实现了所有的方法,我们还需要启动一个gRPC服务器,以便客户端使用我们的服务。以下代码段显示了我们如何为RouteGuide
服务执行此操作:
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
}
/** 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();
}
...
public void start() throws IOException {
server.start();
logger.info("Server started, listening on " + port);
...
}
如你所见,我们用一个ServerBuilder
构建和启动服务器。
为此,我们:
- 使用构建器的
forPort()
方法指定要用于侦听客户端请求的地址和端口。 - 创建我们的服务实现类
RouteGuideService
的一个实例,并将其传递给构建器的addService()
方法。 - 在构建器上调用
build()
和start()
来为我们的服务创建和启动RPC服务器。
创建客户端
在本节中,我们将介绍为RouteGuide
服务创建一个Java客户端。
您可以在grpc-java/examples/src/main/java/io/grpc/examples/RouteGuideClient.java中看到我们完整的示例客户端代码。
创建一个存根
要调用服务方法,我们首先需要创建一个存根,或者说是两个存根:
- 一个阻塞/同步存根: 这意味着RPC调用等待服务器响应,并且要么返回应答,要么造成异常。
- 一个非阻塞/异步存根可以向服务器发起非阻塞调用,应答会异步返回。你可以使用异步存根去发起特定类型的流式调用。
首先,我们需要为我们的存根创建一个gRPC通道,指定要连接到的服务器地址和端口:
public RouteGuideClient(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true));
}
/** Construct client for accessing RouteGuide server using the existing channel. */
public RouteGuideClient(ManagedChannelBuilder<?> channelBuilder) {
channel = channelBuilder.build();
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
我们使用ManagedChannelBuilder
创建通道。
现在我们可以使用该通道来创建我们的存根,使用我们从.proto
生成的RouteGuideGrpc
类中提供的newStub
和newBlockingStub
方法。
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
调用服务方法
现在来看看我们如何调用服务方法。
简单的RPC
与调用本地方法一样简单,在阻塞存根上调用简单的RPCGetFeature
。
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;
}
我们创建并填充请求协议缓冲区对象(在我们的Point
中),将其传递给我们的阻塞存根上的getFeature()
方法,并返回一个Feature
。
如果发生错误,它将被编码为Status
,我们可以从StatusRuntimeException
获取。
服务器端流式RPC
接下来,让我们看一个对于ListFeatures
的服务器端流式调用,这个调用会返回一个地理特征Feature
流:
Rectangle request =
Rectangle.newBuilder()
.setLo(Point.newBuilder().setLatitude(lowLat).setLongitude(lowLon).build())
.setHi(Point.newBuilder().setLatitude(hiLat).setLongitude(hiLon).build()).build();
Iterator<Feature> features;
try {
features = blockingStub.listFeatures(request);
} catch (StatusRuntimeException ex) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
正如你所看到的,它非常类似于我们刚刚看到的简单RPC,除了返回一个Feature
之外,该方法返回一个Iterator
,客户端可以使用它来读取所有返回的Feature
。
客户端流式RPC
现在看看稍微复杂点的东西:我们在客户端流方法RecordRoute
中发送了一个Point
流给服务器并且拿到一个RouteSummary
。为了这个方法,我们需要使用异步存根。
如果您已经阅读了创建服务器,这些可能看起来非常熟悉 - 异步流式RPC将以类似的方式在双方实现。
public void recordRoute(List<Feature> features, int numPoints) throws InterruptedException {
info("*** RecordRoute");
final CountDownLatch finishLatch = new CountDownLatch(1);
StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {
@Override
public void onNext(RouteSummary summary) {
info("Finished trip with {0} points. Passed {1} features. "
+ "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime());
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
logger.log(Level.WARNING, "RecordRoute Failed: {0}", status);
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("Finished RecordRoute");
finishLatch.countDown();
}
};
StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
try {
// Send numPoints points randomly selected from the features list.
Random rand = new Random();
for (int i = 0; i < numPoints; ++i) {
int index = rand.nextInt(features.size());
Point point = features.get(index).getLocation();
info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
RouteGuideUtil.getLongitude(point));
requestObserver.onNext(point);
// Sleep for a bit before sending the next one.
Thread.sleep(rand.nextInt(1000) + 500);
if (finishLatch.getCount() == 0) {
// RPC completed or errored before we finished sending.
// Sending further requests won't error, but they will just be thrown away.
return;
}
}
} catch (RuntimeException e) {
// Cancel RPC
requestObserver.onError(e);
throw e;
}
// Mark the end of requests
requestObserver.onCompleted();
// Receiving happens asynchronously
finishLatch.await(1, TimeUnit.MINUTES);
}
如你所见,为了调用这个方法我们需要创建一个StreamObserver
,它实现了一个特殊的接口,使服务器可以使用其RouteSummary
响应进行调用。在StreamObserver
中,我们:
- 覆写了
onNext()
方法,在服务器把RouteSummary
写入到消息流时,打印出返回的信息。 - 覆写了
onCompleted()
方法(在服务器完成自己的调用时调用)去CountDownLatch
减1,这样我们可以检查服务器是不是完成写入。
然后,我们将StreamObserver
传递给异步存根的recordRoute()
方法,并返回我们自己的StreamObserver
请求观察器,将我们的Point
写入并发送到服务器。
一旦我们完成写Point
,我们使用请求观察者的onCompleted()
方法来告诉gRPC我们已经完成了在客户端的写入。一旦完成,我们检查我们的CountDownLatch
来检查服务器是否已完成响应。
双向流RPC
最后,我们来看看我们的双向流RPCRouteChat()
。
public void routeChat() throws Exception {
info("*** RoutChat");
final CountDownLatch finishLatch = new CountDownLatch(1);
StreamObserver<RouteNote> requestObserver =
asyncStub.routeChat(new StreamObserver<RouteNote>() {
@Override
public void onNext(RouteNote note) {
info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation()
.getLatitude(), note.getLocation().getLongitude());
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
logger.log(Level.WARNING, "RouteChat Failed: {0}", status);
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("Finished RouteChat");
finishLatch.countDown();
}
});
try {
RouteNote[] requests =
{newNote("First message", 0, 0), newNote("Second message", 0, 1),
newNote("Third message", 1, 0), newNote("Fourth message", 1, 1)};
for (RouteNote request : requests) {
info("Sending message \"{0}\" at {1}, {2}", request.getMessage(), request.getLocation()
.getLatitude(), request.getLocation().getLongitude());
requestObserver.onNext(request);
}
} catch (RuntimeException e) {
// Cancel RPC
requestObserver.onError(e);
throw e;
}
// Mark the end of requests
requestObserver.onCompleted();
// Receiving happens asynchronously
finishLatch.await(1, TimeUnit.MINUTES);
}
与我们的客户端流示例一样,我们都获取并返回一个StreamObserver
响应观察器,除了这次我们通过我们的方法的响应观察器发送值,同时服务器仍然在其消息流中写入消息。
这里读写的语法和客户端流以及服务器流方法一样。虽然每一端都会按照它们写入的顺序拿到另一端的消息,客户端和服务器都可以任意顺序读写——流的操作是互不依赖的。
试试看!
按照示例目录README中的说明构建并运行客户端和服务器
参考链接: gRPC Basics - Java & grpc-java