gRPC -- Java 基础教程

本文为 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)

启动服务端

创建客户端

实例化存根(Stub)

调用服务方法

[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.javaPoint.java 等:Protobuf 消息类(序列化 / 反序列化)
  • RouteGuideGrpc.java:gRPC 核心类
    • RouteGuideGrpc.RouteGuideImplBase:服务端基类(含所有服务方法)
    • 客户端存根(Stub):用于调用服务端

创建服务端

实现 RouteGuide 服务,分为两步:

  1. 继承生成的服务基类,实现所有服务方法
  2. 启动 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);
}

动手尝试!

按照示例目录的说明文档构建并运行客户端与服务端。

相关推荐
甲方大人请饶命1 小时前
Java-面向对象进阶(qqbb知识点)
java·开发语言
ChoSeitaku1 小时前
07_static_JavaBean_继承_super/this
java·开发语言
江南十四行1 小时前
并发编程(一)
java·jvm·算法
Dicky-_-zhang1 小时前
自动化运维实战:监控告警与自动化运维的完整方案
java·jvm
hbugs0011 小时前
EVE-NG桥接外网的5种方式
开发语言·网络·php·eve-ng·rstp·流量洞察
wjs20242 小时前
Lua 字符串
开发语言
三品吉他手会点灯2 小时前
C语言学习笔记 - 33.数据类型 - printf函数的详细用法
c语言·开发语言·笔记·学习·算法
知行合一。。。2 小时前
Python--05--面向对象(继承,多态)
android·开发语言·python