Java gRPC do zero

Vamos explorar como implementar gRPC em Java.

gRPC (Google Remote Procedure Call): gRPC é uma arquitetura RPC de código aberto desenvolvida pelo Google para permitir comunicação de alta velocidade entre microsserviços. O gRPC permite que os desenvolvedores integrem serviços escritos em diferentes idiomas. O gRPC usa o formato de mensagens Protobuf (Protocol Buffers), um formato de mensagens altamente eficiente e compactado para serializar dados estruturados.

Para alguns casos de uso, a API gRPC pode ser mais eficiente que a API REST.

Vamos tentar escrever um servidor no gRPC. Primeiro, precisamos escrever vários arquivos .proto que descrevem serviços e modelos (DTO). Para um servidor simples, usaremos ProfileService e ProfileDescriptor.

ProfileService tem a seguinte aparência:

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) {}
}

O gRPC oferece suporte a uma variedade de opções de comunicação cliente-servidor. Nós vamos quebrar todos eles:

  • Chamada normal do servidor – solicitação/resposta.
  • Transmissão do cliente para o servidor.
  • Transmissão do servidor para o cliente.
  • E, claro, o fluxo bidirecional.

O serviço ProfileService usa o ProfileDescriptor, que é especificado na seção de importação:

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 é Long para Java. Deixe o ID do perfil pertencer.
  • String – assim como em Java, esta é uma variável de string.
  Como encontrar o nome de usuário e a senha de login do Tinder

Você pode usar Gradle ou maven para construir o projeto. É mais conveniente para mim usar o maven. E ainda será o código usando maven. Isso é importante o suficiente para dizer porque, para Gradle, a geração futura do .proto será um pouco diferente e o arquivo de compilação precisará ser configurado de maneira diferente. Para escrever um servidor gRPC simples, precisamos apenas de uma dependência:

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

É simplesmente incrível. Este iniciador faz uma tremenda quantidade de trabalho para nós.

O projeto que criaremos será mais ou menos assim:

Precisamos de GrpcServerApplication para iniciar o aplicativo Spring Boot. E GrpcProfileService, que implementará métodos do serviço .proto. Para usar protoc e gerar classes a partir de arquivos .proto gravados, adicione protobuf-maven-plugin a pom.xml. A seção de construção ficará assim:

<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 – especificando o diretório onde os arquivos .proto estão localizados.
  • outputDirectory – selecione o diretório onde os arquivos serão gerados.
  • clearOutputDirectory – um sinalizador indicando para não limpar os arquivos gerados.

Nesta fase, você pode construir um projeto. Em seguida, você precisa ir para a pasta que especificamos no diretório de saída. Os arquivos gerados estarão lá. Agora você pode implementar gradualmente o GrpcProfileService.

  Como usar botões de eco para controlar dispositivos Smarthome

A declaração da classe ficará assim:

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

Anotação GRpcService – Marca a classe como um grpc-service bean.

Como herdamos nosso serviço de ProfileServiceGrpc, ProfileServiceImplBase, podemos substituir os métodos da classe pai. O primeiro método que substituiremos é 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();
    }

Para responder ao cliente, você precisa chamar o método onNext no StreamObserver passado. Depois de enviar a resposta, envie um sinal ao cliente de que o servidor terminou de trabalhar emConcluído. Ao enviar uma solicitação para o servidor getCurrentProfile, a resposta será:

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

Em seguida, vamos dar uma olhada no fluxo do servidor. Com essa abordagem de mensagens, o cliente envia uma solicitação ao servidor, o servidor responde ao cliente com um fluxo de mensagens. Por exemplo, ele envia cinco solicitações em um loop. Quando o envio é concluído, o servidor envia uma mensagem ao cliente sobre a conclusão bem-sucedida do fluxo.

O método de fluxo do servidor substituído ficará assim:

@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();
    }

Assim, o cliente receberá cinco mensagens com um ProfileId, igual ao número da resposta.

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

O fluxo do cliente é muito semelhante ao fluxo do servidor. Só agora o cliente transmite um fluxo de mensagens e o servidor as processa. O servidor pode processar as mensagens imediatamente ou aguardar todas as solicitações do cliente e processá-las.

    @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();
            }
        };
    }

No fluxo do cliente, você precisa retornar o StreamObserver ao cliente, para o qual o servidor receberá mensagens. O método onError será chamado se ocorrer um erro no stream. Por exemplo, ele terminou incorretamente.

  Código promocional GetUpside para usuários existentes: resgate agora

Para implementar um fluxo bidirecional, é necessário combinar a criação de um fluxo do servidor e do cliente.

@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();
            }
        };
    } 

Neste exemplo, em resposta à mensagem do cliente, o servidor retornará um perfil com um pointCount aumentado.

Conclusão

Cobrimos as opções básicas para mensagens entre um cliente e um servidor usando gRPC: fluxo de servidor implementado, fluxo de cliente, fluxo bidirecional.

O artigo foi escrito por Sergey Golitsyn