Начало работы с gRPC-Java

1. Введение

В этой лабораторной работе вы будете использовать gRPC-Java для создания клиента и сервера, которые составят основу приложения для сопоставления маршрутов, написанного на Java.

К концу руководства у вас будет клиент, который подключается к удалённому серверу с помощью gRPC для получения названия или почтового адреса объекта, расположенного в определённых координатах на карте. Полноценное приложение может использовать эту клиент-серверную архитектуру для перечисления или суммирования точек интереса на маршруте.

API сервера определяется в файле Protocol Buffers, который будет использоваться для генерации шаблонного кода для клиента и сервера, чтобы они могли взаимодействовать друг с другом, экономя ваше время и усилия при реализации этой функциональности.

Сгенерированный код учитывает не только сложности взаимодействия между сервером и клиентом, но также сериализацию и десериализацию данных.

Чему вы научитесь

  • Как использовать Protocol Buffers для определения API сервиса.
  • Как создать клиент и сервер на основе gRPC из определения Protocol Buffers с использованием автоматической генерации кода.
  • Понимание клиент-серверного взаимодействия с помощью gRPC.

Эта практическая работа предназначена для разработчиков Java, впервые использующих gRPC, желающих освежить свои знания, а также для всех, кто интересуется разработкой распределённых систем. Опыт работы с gRPC не требуется.

2. Прежде чем начать

Предпосылки

  • JDK версии 8 или выше

Получить код

Чтобы вам не пришлось начинать всё с нуля, эта лабораторная работа предоставляет вам заготовку исходного кода приложения. Следующие шаги покажут вам, как завершить приложение, включая использование плагинов компилятора буфера протокола для генерации шаблонного кода gRPC.

Сначала создайте рабочий каталог codelab и перейдите в него:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Загрузите и распакуйте кодовую лабораторию:

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

Кроме того, вы можете загрузить .zip-файл, содержащий только каталог codelab, и вручную распаковать его.

Если вы не хотите вводить реализацию вручную, готовый исходный код доступен на GitHub .

3. Определите услугу

Первым шагом будет определение службы gRPC приложения, её метода RPC и типов сообщений запросов и ответов с помощью Protocol Buffers . Ваша служба будет предоставлять:

  • Метод RPC, называемый GetFeature , который реализует сервер и вызывает клиент.
  • Типы сообщений Point и Feature представляют собой структуры данных, которыми обмениваются клиент и сервер при использовании метода GetFeature . Клиент предоставляет координаты карты в качестве Point в своём запросе GetFeature к серверу, а сервер отвечает соответствующим Feature , описывающим то, что находится в этих координатах.

Этот метод RPC и его типы сообщений будут определены в файле src/main/proto/routeguide/route_guide.proto предоставленного исходного кода.

Буферы протоколов обычно называются protobuf. Подробнее о терминологии gRPC см. в разделе «Основные концепции, архитектура и жизненный цикл gRPC».

Поскольку в этом примере мы генерируем код Java, мы указали параметр файла java_package и имя для класса Java в нашем .proto :

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Типы сообщений

В файле routeguide/route_guide.proto исходного кода сначала определите тип сообщения Point . Point представляет собой пару координат (широта-долгота) на карте. В этой практической работе используйте целые числа в качестве координат:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Цифры 1 и 2 — это уникальные идентификационные номера для каждого из полей в структуре message .

Затем определите тип сообщения 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 имеет структуру service с именем RouteGuide , которая определяет один или несколько методов, предоставляемых службой приложения.

Добавьте метод rpc GetFeature в определение RouteGuide . Как объяснялось ранее, этот метод ищет название или адрес местоположения по заданному набору координат, поэтому GetFeature должен возвращать Feature для заданной Point :

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Это унарный метод RPC: простой RPC , при котором клиент отправляет запрос серверу и ждет ответа, как при локальном вызове функции.

4. Генерация клиентского и серверного кода

Далее нам нужно сгенерировать клиентский и серверный интерфейсы gRPC из определения сервиса .proto . Для этого мы используем компилятор буфера протокола protoc со специальным плагином gRPC для Java. Для генерации сервисов gRPC вам понадобится компилятор proto3 (поддерживающий синтаксис proto2 и proto3).

При использовании Gradle или Maven плагин сборки protoc может сгенерировать необходимый код в процессе сборки. Инструкции по генерации кода из собственных файлов .proto см. в файле README grpc-java .

Для сборки этого проекта мы предоставили среду 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> : наблюдатель ответов, представляющий собой специальный интерфейс, который сервер может вызывать вместе со своим ответом.

Чтобы ответить клиенту и завершить звонок:

  1. Мы создаём и заполняем объект ответа Feature для возврата клиенту, как указано в определении нашего сервиса. В данном примере это делается в отдельном приватном методе checkFeature() .
  2. Мы используем метод onNext() наблюдателя ответа, чтобы вернуть Feature .
  3. Мы используем метод onCompleted() наблюдателя ответов, чтобы указать, что мы завершили работу с RPC.

Запустить сервер

После реализации всех методов сервиса нам нужно запустить gRPC-сервер, чтобы клиенты могли использовать наш сервис. Включаем в наш шаблон создание объекта ServerBuilder:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Создаём сервис в конструкторе:

  1. Укажите порт, который мы хотим использовать для прослушивания клиентских запросов, используя метод forPort() конструктора (он будет использовать подстановочный адрес).
  2. Создайте экземпляр класса реализации нашей службы RouteGuideService и передайте его методу addService() конструктора.
  3. Вызовите 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, который вызывает 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 .

В основном методе мы:

  1. Создайте экземпляр RouteGuideServer .
  2. Вызовите start() , чтобы активировать RPC-сервер для нашей службы.
  3. Дождитесь остановки службы, вызвав blockUntilShutdown() .
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Создайте клиента

В этом разделе мы рассмотрим создание клиента для нашего сервиса RouteGuide .

Создать заглушку

Для вызова методов сервиса сначала необходимо создать заглушку . Существует два типа заглушек, но для этой практической работы нам понадобится только блокирующая. Эти два типа:

  • блокирующая/синхронная заглушка, которая выполняет вызов RPC и ждет ответа сервера, а затем либо возвращает ответ, либо вызывает исключение.
  • Неблокирующая/асинхронная заглушка, которая выполняет неблокирующие вызовы к серверу, возвращая ответ асинхронно. Некоторые типы потоковых вызовов можно выполнять только с помощью асинхронной заглушки.

Сначала нам нужно создать канал gRPC, а затем использовать этот канал для создания нашей заглушки.

Мы могли бы использовать ManagedChannelBuilder напрямую для создания канала.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Но давайте воспользуемся служебным методом, который принимает строку с hostname:port .

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Теперь мы можем использовать канал для создания блокирующей заглушки. В этой лабораторной работе у нас есть только блокирующие RPC-вызовы, поэтому мы используем метод newBlockingStub , предоставленный в классе RouteGuideGrpc , сгенерированном нами из нашего .proto .

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Методы обслуживания вызовов

Теперь давайте посмотрим, как мы вызываем наши методы обслуживания.

Простой RPC

Вызов простого RPC GetFeature почти так же прост, как вызов локального метода.

Мы создаем и заполняем объект буфера протокола запроса (в нашем случае Point ), передаем его методу 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. Попробуйте!

  1. Внутри каталога start_here выполните следующую команду:
$ ./gradlew installDist

Это скомпилирует ваш код, упакует его в jar-файл и создаст скрипты для запуска примера. Они будут созданы в каталоге build/install/start_here/bin/ . Эти скрипты: route-guide-server и route-guide-client .

Перед запуском клиента сервер должен быть запущен.

  1. Запускаем сервер:
$ ./build/install/start_here/bin/route-guide-server
  1. Запустите клиент:
$ ./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

8. Что дальше?