Пример gRPC в Java

Google выпустила gRPC в качестве новой платформы с открытым исходным кодом в 2015 году, с тех пор она изменила способ обмена данными между сервисами в нескольких центрах обработки данных. Если вы хотите создать действительно независимый от языка микросервис, который будет надежным, высокопроизводительным и легко масштабируемым в распределенной среде. Тогда вам определенно стоит выбрать gRPC.

Что такое gRPC?

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

Струна-2

Проще говоря, вы создаете прототип службы с ее запросом и ответом, разделяете его между сервером и клиентом, и они генерируют соответствующие методы и реализуют их, теперь клиент вызывает метод с запросом, который передается на сервер, метод выполняется сервер и возвращает ответ клиенту, но на стороне клиента это будет выглядеть как работа с локальным методом.

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

Основная особенность

Некоторые важные особенности gRPC:

  1. Он поддерживает более 10 языков, вы можете написать свой сервис на Java, и даже клиент Python сможет получить к нему доступ.
  2. Он использует протокол http 2, который очень эффективен для работы с большими объемами данных.
  3. Он предоставляет очень простое и понятное объявление сервиса.
  4. Он поддерживает двунаправленные потоки.
  5. Он предоставляет множество плагинов, таких как аутентификация, трассировка, балансировка нагрузки и проверка работоспособности.

Зависимости Maven

Давайте добавим зависимости grpc:

    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-all</artifactId>
            <version>1.18.0</version>
        </dependency>
    </dependencies>

Определить сервис

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

Мы начнем с определения метода с параметрами и возвращаемым значением, но это должен быть прототип.

Мы делаем это, создавая файл .proto с использованием буферов протокола , которые используются для описания сообщений.

Итак, мы создаем файл Calculator.proto в /src/main/proto.

syntax = "proto3"; //this directs compiler to use version 3

option java_multiple_files = true; //we want generate different java files after compilation. By default all the classes are generated in a single file.
package in.kuros.grpc; // defines the package structure of generated classes.

//request payload
message OperandRequest {
    int32 X = 1; // message with type, along with tag: 1
    int32 Y = 2; // tag: 2
}

//response payload
message AddResponse {
    int64 result = 1;
}

// service contract
service Calculator {
    rpc add(OperandRequest) returns (AddResponse); // method name: add, parameter type: OperandRequest, response type: AddResponse 
}

Каждому атрибуту необходимо присвоить уникальный номер, называемый тегом. Этот тег используется буфером протокола для представления атрибута вместо использования имени атрибута. Таким образом, в отличие от JSON , где мы каждый раз передавали имя атрибута X , буфер протокола будет использовать число 1 для представления X. Определение полезной нагрузки ответа аналогично запросу.

Итак, в конце после генерации у нас будет три класса (с точки зрения Java): Operands, AddResponse и Calculator, который принимает операнды и возвращает addResponse;

Генерация файлов классов для Java.

Теперь мы передаем файл HelloService.proto в протокол компилятора буфера протокола для создания файлов Java. Есть несколько способов вызвать это.

Компилятор буфера протокола

Загрузите компилятор и следуйте readme.txt.

Используйте команду для генерации кода

$ protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/proto/calculator.proto

Плагин компилятора Maven

Вы не хотите каждый раз выполнять команду для генерации кода, поэтому мы будем использовать плагин maven для выполнения во время сборки.

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>
                        com.google.protobuf:protoc:3.5.1:exe:${os.detected.classifier}
                    </protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>
                        io.grpc:protoc-gen-grpc-java:1.18.0:exe:${os.detected.classifier}
                    </pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Расширение / плагин os-maven- plugin генерирует различные полезные свойства проекта, зависящие от платформы, такие как ${os.detected.classifier}

Сгенерированные файлы

После запуска генерации некоторые ключевые файлы будут созданы в папке /target/generated-sources/protobuf.

  • OperandRequest.java — содержит определение OperandRequest.
  • AddResponse.java — содержит определение AddResponse.
  • CalculatorGrpc.java — содержит абстрактный внутренний класс CalculatorImplBase, который предоставляет оболочку реализации.

Реализация на стороне сервера

Теперь пришло время написать логику сложения на стороне сервера. поэтому мы создадим класс реализации CalculatorImpl , который расширит CalculatorImplBase и обеспечит реализацию.

import in.kuros.grpc.AddResponse;
import in.kuros.grpc.CalculatorGrpc.CalculatorImplBase;
import in.kuros.grpc.OperandRequest;
import io.grpc.stub.StreamObserver;

public class CalculatorImpl extends CalculatorImplBase {

    @Override
    public void add(final OperandRequest request, final StreamObserver<AddResponse> responseObserver) {
        final long sum = request.getX() + request.getY();

        final AddResponse addResponse = AddResponse
                .newBuilder()
                .setResult(sum)
                .build();

        try {
            System.out.println("Sleeping to 5 sec");
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        responseObserver.onNext(addResponse);
        responseObserver.onCompleted();
    }
}

Мы предоставили 5-секундный режим сна для имитации длительных операций.

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

Далее нам нужно запустить сервер gRPC для прослушивания входящих запросов:

import io.grpc.Server;
import io.grpc.ServerBuilder;

public class GrpcServer {

    public static void main(String[] args) throws Exception {
        final Server server = ServerBuilder.forPort(8080)
                .addService(new CalculatorImpl())
                .build();

        server.start();
        server.awaitTermination();
    }
}

Итак, здесь мы создали подачу для прослушивания входящего запроса на порту 8080, зарегистрировали наш сервис калькулятора. В нашем примере мы вызовем awaitTermination(), чтобы сервер работал на переднем плане, блокируя приглашение.

Создание клиента

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

Для создания канала мы используем ManagedChannelBuilder .

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
                .usePlaintext()
                .build();

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

Синхронизация вызовов

Создаем блокирующую заглушку

    final CalculatorGrpc.CalculatorBlockingStub blockingStub = CalculatorGrpc.newBlockingStub(channel);

    System.out.println("Making blocking call");
    final AddResponse blockResponse = blockingStub.add(OperandRequest.newBuilder().setX(10).setY(20).build());
    System.out.println("blocking call result: " + blockResponse.getResult());

Выполнение асинхронных вызовов

Мы используем метод newStub для выполнения вызовов aysnc, но нам нужно предоставить ему реализацию StreamObserver.

    final CalculatorGrpc.CalculatorStub asyncStub = CalculatorGrpc.newStub(channel);
        
    StreamObserver<AddResponse> streamObserver = new StreamObserver<AddResponse>() {
        public void onNext(final AddResponse addResponse) {
            System.out.println("async call result: " + addResponse.getResult());
        }
    
        public void onError(final Throwable throwable) {
            System.out.println(throwable);
        }
    
        public void onCompleted() {
            System.out.println("Async call stopped listening");
        }
    };


    System.out.println("Making blocking call");
    asyncStub.add(OperandRequest.newBuilder().setX(10).setY(20).build(), streamObserver);
    System.out.println("Async invoked" + blockResponse.getResult());

    channel.awaitTermination(10, TimeUnit.SECONDS);

полный код клиента:

import in.kuros.grpc.AddResponse;
import in.kuros.grpc.CalculatorGrpc;
import in.kuros.grpc.OperandRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;

import java.util.concurrent.TimeUnit;

public class GrpcClient {

    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
                .usePlaintext()
                .build();

        final CalculatorGrpc.CalculatorBlockingStub blockingStub = CalculatorGrpc.newBlockingStub(channel);

        System.out.println("Making blocking call");
        final AddResponse blockResponse = blockingStub.add(OperandRequest.newBuilder().setX(10).setY(20).build());
        System.out.println("blocking call result: " + blockResponse.getResult());


        final CalculatorGrpc.CalculatorStub asyncStub = CalculatorGrpc.newStub(channel);

        StreamObserver<AddResponse> streamObserver = new StreamObserver<AddResponse>() {
            public void onNext(final AddResponse addResponse) {
                System.out.println("async call result: " + addResponse.getResult());
            }

            public void onError(final Throwable throwable) {
                System.out.println(throwable);
            }

            public void onCompleted() {
                System.out.println("Async call stopped listening");
            }
        };


        System.out.println("Making blocking call");
        asyncStub.add(OperandRequest.newBuilder().setX(10).setY(20).build(), streamObserver);
        System.out.println("Async invoked" + blockResponse.getResult());

        channel.awaitTermination(10, TimeUnit.SECONDS);

    }
}

Далее нам нужно создать заглушку, которую мы будем использовать для фактического удаленного вызова hello(). Заглушка — это основной способ взаимодействия клиентов с сервером . При использовании заглушек автоматического создания класс заглушки будет иметь конструкторы для переноса канала.

Заключение

В этом посте мы узнали, как работать с сервером/клиентом gRPC в Java.