Java gRPC с нуля

Опубликовано: 2021-07-16

Давайте посмотрим, как реализовать gRPC в Java.

gRPC (удаленный вызов процедур Google): gRPC - это архитектура RPC с открытым исходным кодом, разработанная Google для обеспечения высокоскоростной связи между микросервисами. gRPC позволяет разработчикам интегрировать сервисы, написанные на разных языках. gRPC использует формат обмена сообщениями Protobuf (протокол буферов), высокоэффективный, плотно упакованный формат обмена сообщениями для сериализации структурированных данных.

Для некоторых случаев использования gRPC API может быть более эффективным, чем REST API.

Попробуем написать сервер на gRPC. Во-первых, нам нужно написать несколько файлов .proto которые описывают сервисы и модели (DTO). Для простого сервера мы будем использовать ProfileService и ProfileDescriptor.

ProfileService выглядит так:

 syntax = "proto3"; package com.deft.grpc; import "google/protobuf/empty.proto"; import "profile_descriptor.proto"; service ProfileService { rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {} rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {} rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {} rpc biDirectionalStream (stream ProfileDescriptor) returns (stream ProfileDescriptor) {} }

gRPC поддерживает множество вариантов связи клиент-сервер. Разобьем их все:

  • Обычный вызов сервера - запрос / ответ.
  • Стриминг от клиента к серверу.
  • Потоковая передача от сервера к клиенту.
  • И, конечно же, двунаправленный поток.

Сервис ProfileService использует ProfileDescriptor, который указан в разделе импорта:

 syntax = "proto3"; package com.deft.grpc; message ProfileDescriptor { int64 profile_id = 1; string name = 2; }
  • int64 для Java - длинный. Пусть идентификатор профиля принадлежит.
  • String - как и в Java, это строковая переменная.

Вы можете использовать Gradle или maven для сборки проекта. Мне удобнее использовать maven. А дальше будет код с использованием maven. Это достаточно важно, чтобы сказать, потому что для Gradle будущее поколение .proto будет немного другим, и файл сборки нужно будет настроить по-другому. Чтобы написать простой сервер gRPC, нам нужна только одна зависимость:

 <dependency> <groupId>io.github.lognet</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>4.5.4</version> </dependency>

Это просто невероятно. Этот стартер выполняет за нас колоссальный объем работы.

Проект, который мы создадим, будет выглядеть примерно так:

Нам понадобится GrpcServerApplication для запуска приложения Spring Boot. И GrpcProfileService, который будет реализовывать методы из службы .proto . Чтобы использовать протокол и сгенерировать классы из написанных файлов .proto, добавьте protobuf-maven-plugin в pom.xml. Раздел сборки будет выглядеть так:

 <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.6.2</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.6.1</version> <configuration> <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot> <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory> <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact> <clearOutputDirectory>false</clearOutputDirectory> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
  • protoSourceRoot - указание каталога, в котором находятся файлы .proto.
  • outputDirectory - выберите каталог, в котором будут созданы файлы.
  • clearOutputDirectory - флаг, указывающий не очищать сгенерированные файлы.

На этом этапе вы можете построить проект. Далее вам нужно перейти в папку, которую мы указали в выходном каталоге. Сгенерированные файлы будут там. Теперь вы можете постепенно внедрять GrpcProfileService .

Объявление класса будет выглядеть так:

 @GRpcService public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Аннотация GRpcService - помечает класс как bean-компонент grpc-service.

Поскольку мы наследуем наш сервис от ProfileServiceGrpc , ProfileServiceImplBase , мы можем переопределить методы родительского класса. Первый метод, который мы переопределим, - это getCurrentProfile :

 @Override public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { System.out.println("getCurrentProfile"); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(1) .setName("test") .build()); responseObserver.onCompleted(); }

Чтобы ответить клиенту, вам необходимо вызвать метод onNext на переданном StreamObserver. После отправки ответа, посылает сигнал к клиенту , что сервер закончил работу OnCompleted. При отправке запроса на сервер getCurrentProfile ответ будет таким:

 { "profile_id": "1", "name": "test" }

Далее давайте посмотрим на поток сервера. При таком подходе к обмену сообщениями клиент отправляет запрос на сервер, сервер отвечает клиенту потоком сообщений. Например, он отправляет пять запросов в цикле. Когда отправка завершена, сервер отправляет клиенту сообщение об успешном завершении потока.

Переопределенный метод потока сервера будет выглядеть так:

 @Override public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { for (int i = 0; i < 5; i++) { responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(i) .build()); } responseObserver.onCompleted(); }

Таким образом, клиент получит пять сообщений с ProfileId, равным номеру ответа.

 { "profile_id": "0", "name": "" } { "profile_id": "1", "name": "" } … { "profile_id": "4", "name": "" }

Клиентский поток очень похож на серверный поток. Только теперь клиент передает поток сообщений, а сервер их обрабатывает. Сервер может обрабатывать сообщения немедленно или ждать всех запросов от клиента, а затем обрабатывать их.

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) { return new StreamObserver<>() { @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

В клиентском потоке вам нужно вернуть StreamObserver клиенту, которому сервер будет получать сообщения. Метод onError будет вызван, если в потоке произошла ошибка. Например, он завершился некорректно.

Чтобы реализовать двунаправленный поток, необходимо совместить создание потока от сервера и клиента.

 @Override public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream( StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) { return new StreamObserver<>() { int pointCount = 0; @Override public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) { log.info("biDirectionalStream, pointCount {}", pointCount); responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor .newBuilder() .setProfileId(pointCount++) .build()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { responseObserver.onCompleted(); } }; }

В этом примере в ответ на сообщение клиента сервер вернет профиль с увеличенным значением pointCount .

Вывод

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

Статья написана Сергеем Голицыным.