«1. Введение

gRPC — это высокопроизводительная среда RPC с открытым исходным кодом, первоначально разработанная Google. Это помогает избавиться от стандартного кода и помогает в подключении многоязычных сервисов в центрах обработки данных и между ними.

2. Обзор

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

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

  1. Define a service in a .proto file
  2. Generate server and client code using the protocol buffer compiler
  3. Create the server application, implementing the generated service interfaces and spawning the gRPC server
  4. Create the client application, making RPC calls using generated stubs

Давайте определим простой HelloService, который возвращает приветствия в обмен на имя и фамилию.

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

Давайте добавим зависимости grpc-netty, grpc-protobuf и grpc-stub:

<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty</artifactId>
    <version>1.16.1</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.16.1</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.16.1</version>
</dependency>

4. Определение службы

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

Это делается в файле .proto с использованием буферов протокола. Они также используются для описания структуры сообщений полезной нагрузки.

4.1. Основные конфигурации

Давайте создадим файл HelloService.proto для нашего образца HelloService. Начнем с добавления нескольких основных деталей конфигурации:

syntax = "proto3";
option java_multiple_files = true;
package org.baeldung.grpc;

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

Наконец, мы указываем пакет, который мы хотим использовать для наших сгенерированных классов Java.

4.2. Определение структуры сообщения

Далее мы определяем сообщение:

message HelloRequest {
    string firstName = 1;
    string lastName = 2;
}

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

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

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

Обратите внимание, что мы можем использовать один и тот же тег для нескольких типов сообщений:

message HelloResponse {
    string greeting = 1;
}

4.3. Определение контракта службы

Наконец, давайте определим контракт службы. Для нашего HelloService мы определяем операцию hello():

service HelloService {
    rpc hello(HelloRequest) returns (HelloResponse);
}

Операция hello() принимает унарный запрос и возвращает унарный ответ. gRPC также поддерживает потоковую передачу, добавляя префикс ключевого слова stream к запросу и ответу.

5. Генерация кода

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

5.1. Использование компилятора буфера протокола

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

Кроме того, нам необходимо получить плагин gRPC Java Codegen.

Наконец, мы можем использовать следующую команду для генерации кода:

protoc --plugin=protoc-gen-grpc-java=$PATH_TO_PLUGIN -I=$SRC_DIR 
  --java_out=$DST_DIR --grpc-java_out=$DST_DIR $SRC_DIR/HelloService.proto

5.2. Использование плагина Maven

Как разработчик, вы хотели бы, чтобы генерация кода была тесно интегрирована с вашей системой сборки. gRPC предоставляет плагин protobuf-maven-plugin для системы сборки 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.3.0:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>
          io.grpc:protoc-gen-grpc-java:1.4.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}

6 Создание сервера

Независимо от того, какой метод вы используете для генерации кода, будут сгенерированы следующие ключевые файлы:

    HelloRequest.java — содержит определение типа HelloRequest HelloResponse.java — содержит тип HelleResponse определение HelloServiceImplBase.java — содержит абстрактный класс HelloServiceImplBase, обеспечивающий реализацию всех операций, которые мы определили в интерфейсе службы

6.1. Переопределение базового класса службы

Реализация абстрактного класса HelloServiceImplBase по умолчанию заключается в выдаче исключения времени выполнения io.grpc.StatusRuntimeException, сообщающего, что метод не реализован.

«Мы расширим этот класс и переопределим метод hello(), упомянутый в нашем определении службы:

public class HelloServiceImpl extends HelloServiceImplBase {

    @Override
    public void hello(
      HelloRequest request, StreamObserver<HelloResponse> responseObserver) {

        String greeting = new StringBuilder()
          .append("Hello, ")
          .append(request.getFirstName())
          .append(" ")
          .append(request.getLastName())
          .toString();

        HelloResponse response = HelloResponse.newBuilder()
          .setGreeting(greeting)
          .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Если мы сравним сигнатуру hello() с той, которую мы написали в файле HellService.proto, мы заметим, что он не возвращает HelloResponse. Вместо этого он принимает второй аргумент как StreamObserver\u003cHelloResponse\u003e, который является наблюдателем ответа, обратным вызовом для сервера, чтобы вызвать его ответ.

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

gRPC использует конструкторы для создания объектов. Мы используем HelloResponse.newBuilder() и устанавливаем текст приветствия для создания объекта HelloResponse. Мы устанавливаем этот объект в метод onNext() объекта responseObserver, чтобы отправить его клиенту.

Наконец, нам нужно вызвать onCompleted(), чтобы указать, что мы закончили работу с RPC, иначе соединение будет зависать, и клиент будет просто ждать поступления дополнительной информации.

6.2. Запуск сервера Grpc

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

public class GrpcServer {
    public static void main(String[] args) {
        Server server = ServerBuilder
          .forPort(8080)
          .addService(new HelloServiceImpl()).build();

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

Здесь мы снова используем сборщик для создания сервера gRPC на порту 8080 и добавляем службу HelloServiceImpl, которую мы определенный. start() запустит сервер. В нашем примере мы будем вызывать awaitTermination(), чтобы сервер работал на переднем плане, блокируя приглашение.

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

gRPC предоставляет структуру канала, которая абстрагирует базовые детали, такие как соединение, пул соединений, балансировка нагрузки и т. д.

Мы создадим канал, используя ManagedChannelBuilder. Здесь мы указываем адрес сервера и порт.

Мы будем использовать обычный текст без какого-либо шифрования:

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

        HelloServiceGrpc.HelloServiceBlockingStub stub 
          = HelloServiceGrpc.newBlockingStub(channel);

        HelloResponse helloResponse = stub.hello(HelloRequest.newBuilder()
          .setFirstName("Baeldung")
          .setLastName("gRPC")
          .build());

        channel.shutdown();
    }
}

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

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

Наконец, пришло время сделать вызов hello() RPC. Здесь мы передаем HelloRequest. Мы можем использовать автоматически сгенерированные установщики для установки атрибутов firstName, lastName объекта HelloRequest.

Мы возвращаем объект HelloResponse, возвращенный с сервера.

8. Заключение

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

Как обычно, вы найдете исходники на GitHub.