Java gRPC з нуля

Давайте дослідимо, як реалізувати 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 — це Long для Java. Нехай ідентифікатор профілю належить.
  • Рядок – як і в Java, це рядкова змінна.
  Як додати умовне форматування до комірок у таблицях Google

Ви можете використовувати 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. Щоб використовувати protoc і створювати класи з написаних файлів .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.

  Redecor Викупити коди на золото

Оголошення класу буде виглядати так:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Анотація GRpcService – позначає клас як компонент 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. Після надсилання відповіді надішліть клієнту сигнал про те, що сервер завершив роботу на Completed. При надсиланні запиту на сервер 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 буде викликаний, якщо в потоці сталася помилка. Наприклад, він завершився неправильно.

  6 найкращих програм ECAD для проектування електронних виробів

Щоб реалізувати двонаправлений потік, необхідно поєднати створення потоку від сервера та клієнта.

@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: реалізований серверний потік, клієнтський потік, двонаправлений потік.

Статтю написав Сергій Голіцин