Структура проекта будет выглядеть следующим образом:
Я буду использовать maven для сборки проекта. Это важное уточнение, потому как будущая генерация .proto файлов будет отличаться для gradle например.
Для написания простого приложения на gRPC мы можем использовать всего одну зависимость:
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>4.5.4</version>
</dependency>
Отличный Spring Boot стартер для старта нашего gRPC сервера.
После загрузки зависимостей, нам потребуется написать .proto файлы. В proto хранятся описания сервисов и ДТО для запроса/ответа. Все пишется интуитивно понятно:
ProfileDescriptor - это условное описание нашего профиля/юзера, то что мы будем использовать для обмена между сервером и клиентом.
int64 - для Java это тип Long. Допустим id профиля будут long.
string - также как и в Java это строковая переменная.
Отлично у нас есть ДТО для обмена сообщениями. Теперь нам необходим сервис:
gRPC поддерживает несколько видов взаимодействия клиент/сервер:
Запрос-ответ
Клиент стрим
Сервер стрим
Двунаправленный стрим
Подробнее о таком взаимодействии рассказано тут - https://habr.com/en/post/565020/
После того как у нас есть описанный сервис, нам понадобится сгенерировать необходимые классы. Для этого в pom.xml нам в build секцию потребуется добавить плагин:
<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 - флаг указывающий не очищать сгенерированные файлы.
После того как вы выполните сборку проекта, можно будет раскрыть папку target и найти следующую структуру:
Если вы получили примерно такую структуру, то поздравляю. Все идет по плану и у нас все получается. Если что то пошло не так, то можно посмотреть на код проекта https://github.com/deft1991/petproject/tree/master/grpc-server.
Итак, мы получили сгенерированные сервисы. Теперь нам необходимо их использовать.
Напишем свой GrpcProfileService, который будет наследовать ProfileServiceGrpc.ProfileServiceImplBase. Прекрасно. У нас готов основной сервис.
Теперь можно переопределять методы, как это сделано на следующем скриншоте :
Можно заметить, что над сервисом стоит аннотация GRpcService. Эта аннотация помечает, что класс должен быть зарегистрирован как gRPC бин.
Вернемся к нашему методу. Пока я переопределил всего 1 метод - метод, который получает профиль. Для ответа клиенту мы используем responseObserver. Что бы сообщение ушло на клиент мы вызываем responseObserver.onNext и передаем тело сообщения. В нашем случае это ProfileDescriptorOuterClass.ProfileDescriptor. У всех сгенерированных сообщений сразу генерируется Builder и при помощи него мы можем “сблидить” объект. После отправки сообщения, необходимо отправить responseObserver.onCompleted - уведомление об успешном завершении стрима.
Для проверки и тестовой отправки сообщений в REST используется обычно Postman. Также с его помощью можно отправить gRPC запросы. Но для меня более удобным является BloomRPC. Ему нужно указать путь к proto файлам и хост, на который отправлять запрос.
При оправке запроса getCurrentProfile мы получим:
{
"profile_id": "1",
"name": "test"
}
Отлично. Давайте двинемся дальше и разберем client stream. При таком типе запросов, клиент может посылать сообщения запросы, а сервер реагировать на каждое из них и/или, при поступлении всех клиентских сообщений, обработать их. В нашем случае предопределенный метод будет выглядеть так:
Мы возвращаем новый StreamObserver клиенту. Так как StreamObserver - это интерфейс, то нам необходимо реализовать его методы. Нас пока интересует метод onNext. В этот метод будут приходить сообщения от клиента. При запуске кода и посылке сообщений от клиента на сервер, можно будет заметить, что счетчик pointCount постоянно увеличивается и в консоль выводится сообщение.
Server stream. В данном типе запросов, после того как клиент послал сообщение запрос, сервер начинает посылать данные для клиента. Это безумно удобный механизм общения между клиентом и сервером. При помощи данной возможности мы можем рассылать клиентам системные уведомления, события и нотификации. Простой код реализации:
После открытия стрима, сервер посылает 5 ответов. В каком последующем ответе найди профиля увеличивается.
После того как мы разобрали стримы с клиента на сервер и сервера на клиент, мы можем приступить к рассмотрению bidirectional streams. В таком типе запросов клиент и сервер могут постоянно обмениваться сообщениями не блокируя друг друга.
В нашем примере в ответ на сообщение клиента, сервер будет посылать новый профиль с увеличенным счетчиком pointCount.
Если у вас возникли сложности с воспроизведением данного примера, то вы можете воспользоваться репозиторием https://github.com/deft1991/petproject/tree/master/grpc-server , где лежит код приведенный в данной статье.
Мы разобрали основные момменты, которые стоит выполнить, что бы написать сервер с использованием gRPC. Посмотрели основные варианты взаимодействия клиента и сервера. Для тестирования я использую BloomRPC.
А вы используете gRPC в проде? Какие у вас ключевые пункты по реализации сервера с использованием gRPC?
Всем приветы. В прошлом посте мы сравнили gRPC и REST. И собственно, прежде чем писать новый сервер на gRPC, давайте попробуем понять нужен ли он нам действительно. Нашей команде была необходима кодогенерация на разные языки программирования. На эту роль неплохо подходил Swagger, Thrift и gRPC со встроенным кодогенератором. От Thrift, спустя какое-то время, пришлось отказаться, из-за его особенностей и сложностей поддержи на c# (по-моему это была основная причина отказа). Дальше был выбор между Swagger + REST и gRPC. В целом оба варианта хороши, но если мы думаем гонять много и часто данные между клиентом и сервером, то почему бы не протестировать gRPC?