本文为 Java 开发者提供 gRPC 入门指南
目录
[Java 基础教程](#Java 基础教程)
[为什么使用 gRPC?](#为什么使用 gRPC?)
[实现 RouteGuide 服务](#实现 RouteGuide 服务)
[1. 简单 RPC:GetFeature](#1. 简单 RPC:GetFeature)
[2. 服务端流式 RPC:ListFeatures](#2. 服务端流式 RPC:ListFeatures)
[3. 客户端流式 RPC:RecordRoute](#3. 客户端流式 RPC:RecordRoute)
[4. 双向流式 RPC:RouteChat](#4. 双向流式 RPC:RouteChat)
[1. 简单 RPC:GetFeature](#1. 简单 RPC:GetFeature)
[2. 服务端流式 RPC:ListFeatures](#2. 服务端流式 RPC:ListFeatures)
[3. 客户端流式 RPC:RecordRoute](#3. 客户端流式 RPC:RecordRoute)
[4. 双向流式 RPC:RouteChat](#4. 双向流式 RPC:RouteChat)
Java 基础教程
本文为 Java 开发者提供 gRPC 入门指南
通过学习本示例,你将掌握:
- 在
.proto文件中定义服务 - 使用协议缓冲区编译器生成客户端与服务端代码
- 使用 Java gRPC API 编写简单的服务端与客户端
本文假设你已阅读 gRPC 简介,并熟悉协议缓冲区(Protobuf)。教程示例使用 proto3 语法,可参考 proto3 语言指南与 Java 生成代码指南了解详情。
为什么使用 gRPC?
本示例为简易路线地图应用,客户端可获取路线上的地标信息、生成路线汇总,并与服务端及其他客户端交换路况等路线信息。
借助 gRPC,只需在 .proto 文件中定义一次服务,即可生成 gRPC 支持的任意语言的客户端与服务端,可运行于大型数据中心服务器、平板等各类环境 ------ 不同语言与环境间的通信复杂性均由 gRPC 处理。同时还能享受协议缓冲区的优势:高效序列化、简洁接口定义、易接口升级。
示例代码与环境准备
教程示例代码位于:grpc/grpc-java/examples/src/main/java/io/grpc/examples/routeguide
克隆指定版本的 grpc-java 仓库:
git clone -b v1.81.0 --depth 1 https://github.com/grpc/grpc-java
进入示例目录:
cd grpc-java/examples
定义服务
gRPC 的第一步是使用协议缓冲区 定义 gRPC 服务、请求与响应类型。完整 .proto 文件见:grpc-java/examples/src/main/proto/route_guide.proto
本示例生成 Java 代码,因此在 .proto 中指定 java_package 选项:
option java_package = "io.grpc.examples.routeguide";
该选项指定生成 Java 类的包名。若未显式指定,默认使用 proto 包名,但 proto 包名通常不符合 Java 规范(不以反向域名开头)。该选项仅对 Java 生效,其他语言生成代码时不受影响。
定义服务方法
在 .proto 文件中用 service 关键字定义服务:
service RouteGuide { ...}
随后在服务内定义 rpc 方法,指定请求与响应类型。gRPC 支持 4 种服务方法 ,均在 RouteGuide 服务中使用:
简单 RPC:客户端发送请求,等待服务端响应,类似普通函数调用
// 获取指定位置的地标
rpc GetFeature(Point) returns (Feature) {}
服务端流式 RPC :客户端发请求,服务端返回消息流,客户端持续读取直到流结束
// 获取指定矩形范围内的地标,结果流式返回
rpc ListFeatures(Rectangle) returns (stream Feature) {}
客户端流式 RPC :客户端发送消息流,服务端接收完所有消息后返回单个响应
// 接收客户端路线点流,返回路线汇总
rpc RecordRoute(stream Point) returns (RouteSummary) {}
双向流式 RPC :客户端与服务端同时发送消息流,双向流独立读写
// 双向交换路线备注
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
消息类型定义
.proto 文件同时定义服务方法所需的请求 / 响应消息类型,例如 Point:
// 经纬度坐标(E7 格式:度数 × 10^7 取整)
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
生成客户端与服务端代码
使用协议缓冲区编译器 protoc + gRPC Java 插件,从 .proto 服务定义生成代码。需使用 proto3 编译器(兼容 proto2/proto3 语法)。
Gradle/Maven 可通过构建插件自动生成代码,详情见 grpc-java 说明文档。
生成的 Java 类包括:
Feature.java、Point.java等:Protobuf 消息类(序列化 / 反序列化)RouteGuideGrpc.java:gRPC 核心类RouteGuideGrpc.RouteGuideImplBase:服务端基类(含所有服务方法)- 客户端存根(Stub):用于调用服务端
创建服务端
实现 RouteGuide 服务,分为两步:
- 继承生成的服务基类,实现所有服务方法
- 启动 gRPC 服务器,监听客户端请求
服务端代码见:
grpc-java/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java
实现 RouteGuide 服务
创建 RouteGuideService 类,继承 RouteGuideGrpc.RouteGuideImplBase:
java
private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase { ... }
1. 简单 RPC:GetFeature
接收客户端坐标,返回对应地标:
java
@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;
}
}
return Feature.newBuilder().setName("").setLocation(location).build();
}
request:客户端请求(Point)responseObserver:响应回调接口onNext():发送响应onCompleted():结束 RPC
2. 服务端流式 RPC:ListFeatures
返回指定矩形内的所有地标流:
java
@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.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();
}
遍历地标库,筛选符合条件的地标,逐个通过 onNext() 发送。
3. 客户端流式 RPC:RecordRoute
接收客户端坐标流,返回路线汇总:
java
@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++;
if (previous != null) distance += calcDistance(previous, point);
previous = point;
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "RecordRoute error", 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<Point> 接收客户端流,onCompleted() 后生成汇总响应。
4. 双向流式 RPC:RouteChat
双向交换路线备注:
java
@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());
for (RouteNote prevNote : notes.toArray(new RouteNote[0])) {
responseObserver.onNext(prevNote);
}
notes.add(note);
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "RouteChat error", t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
双向流独立读写,顺序保持。
启动服务端
使用 ServerBuilder 构建并启动服务器:
java
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
}
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);
}
创建客户端
客户端代码见:grpc-java/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideClient.java
实例化存根(Stub)
客户端需创建阻塞存根 和异步存根:
- 阻塞存根:同步调用,等待响应返回
- 异步存根:非阻塞调用,流式 RPC 必须使用
java
public RouteGuideClient(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port).usePlaintext());
}
public RouteGuideClient(ManagedChannelBuilder<?> channelBuilder) {
channel = channelBuilder.build();
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
调用服务方法
1. 简单 RPC:GetFeature
阻塞调用,直接获取响应:
java
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;
}
2. 服务端流式 RPC:ListFeatures
阻塞调用,通过迭代器读取流:
java
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 e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
3. 客户端流式 RPC:RecordRoute
异步调用,发送坐标流:
java
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("行程完成:{0} 个点,途经 {1} 个地标,行驶 {2} 米,耗时 {3} 秒",
summary.getPointCount(), summary.getFeatureCount(),
summary.getDistance(), summary.getElapsedTime());
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
logger.log(Level.WARNING, "RecordRoute 失败:{0}", status);
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("RecordRoute 完成");
finishLatch.countDown();
}
};
StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
try {
Random rand = new Random();
for (int i = 0; i < numPoints; ++i) {
int index = rand.nextInt(features.size());
Point point = features.get(index).getLocation();
info("访问坐标:{0}, {1}", RouteGuideUtil.getLatitude(point), RouteGuideUtil.getLongitude(point));
requestObserver.onNext(point);
Thread.sleep(rand.nextInt(1000) + 500);
if (finishLatch.getCount() == 0) return;
}
} catch (RuntimeException e) {
requestObserver.onError(e);
throw e;
}
requestObserver.onCompleted();
finishLatch.await(1, TimeUnit.MINUTES);
}
4. 双向流式 RPC:RouteChat
异步调用,双向发送消息:
java
public void routeChat() throws Exception {
info("*** RouteChat");
final CountDownLatch finishLatch = new CountDownLatch(1);
StreamObserver<RouteNote> requestObserver = asyncStub.routeChat(new StreamObserver<RouteNote>() {
@Override
public void onNext(RouteNote note) {
info("收到消息:\"{0}\" 于 {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 失败:{0}", status);
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("RouteChat 完成");
finishLatch.countDown();
}
});
try {
RouteNote[] requests = {
newNote("第一条消息", 0, 0),
newNote("第二条消息", 0, 1),
newNote("第三条消息", 1, 0),
newNote("第四条消息", 1, 1)
};
for (RouteNote request : requests) {
info("发送消息:\"{0}\" 于 {1}, {2}",
request.getMessage(), request.getLocation().getLatitude(), request.getLocation().getLongitude());
requestObserver.onNext(request);
}
} catch (RuntimeException e) {
requestObserver.onError(e);
throw e;
}
requestObserver.onCompleted();
finishLatch.await(1, TimeUnit.MINUTES);
}
动手尝试!
按照示例目录的说明文档构建并运行客户端与服务端。