Primeiros passos com o gRPC-Java

1. Introdução

Neste codelab, você vai usar o gRPC-Java para criar um cliente e um servidor que formam a base de um aplicativo de mapeamento de rotas escrito em Java.

Ao final do tutorial, você terá um cliente que se conecta a um servidor remoto usando o gRPC para receber o nome ou o endereço postal do que está localizado em coordenadas específicas em um mapa. Um aplicativo completo pode usar esse design cliente-servidor para enumerar ou resumir pontos de interesse ao longo de um trajeto.

A API do servidor é definida em um arquivo de buffers de protocolo, que será usado para gerar código boilerplate para o cliente e o servidor, permitindo que eles se comuniquem entre si e economizando tempo e esforço na implementação dessa funcionalidade.

Esse código gerado cuida não apenas das complexidades da comunicação entre o servidor e o cliente, mas também da serialização e desserialização de dados.

O que você vai aprender

  • Como usar buffers de protocolo para definir uma API de serviço.
  • Como criar um cliente e um servidor baseados em gRPC com uma definição de buffers de protocolo usando a geração automática de código.
  • Entendimento da comunicação cliente-servidor com gRPC.

Este codelab é destinado a desenvolvedores Java que não conhecem o gRPC ou querem relembrar o assunto, além de qualquer pessoa interessada em criar sistemas distribuídos. Não é necessário ter experiência com gRPC.

2. Antes de começar

Pré-requisitos

  • JDK versão 8 ou mais recente

Acessar o código

Para que você não precise começar do zero, este codelab oferece um scaffold do código-fonte do aplicativo para você concluir. As etapas a seguir mostram como concluir o aplicativo, incluindo o uso dos plug-ins do compilador de buffer de protocolo para gerar o código gRPC boilerplate.

Primeiro, crie o diretório de trabalho do codelab e use cd para acessar ele:

mkdir grpc-java-getting-started && cd grpc-java-getting-started

Faça o download e extraia o codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here

Como alternativa, baixe o arquivo .zip que contém apenas o diretório do codelab e descompacte-o manualmente.

O código-fonte completo está disponível no GitHub se você quiser pular a digitação de uma implementação.

3. Definir o serviço

A primeira etapa é definir o serviço gRPC do aplicativo, o método RPC e os tipos de mensagens de solicitação e resposta usando buffers de protocolo. Seu serviço vai oferecer:

  • Um método RPC chamado GetFeature que o servidor implementa e o cliente chama.
  • Os tipos de mensagem Point e Feature, que são estruturas de dados trocadas entre o cliente e o servidor ao usar o método GetFeature. O cliente fornece coordenadas do mapa como um Point na solicitação GetFeature ao servidor, e o servidor responde com um Feature correspondente que descreve o que está localizado nessas coordenadas.

Esse método RPC e os tipos de mensagem dele serão definidos no arquivo src/main/proto/routeguide/route_guide.proto do código-fonte fornecido.

Os buffers de protocolo são conhecidos como protobufs. Para mais informações sobre a terminologia do gRPC, consulte Conceitos principais, arquitetura e ciclo de vida do gRPC.

Como estamos gerando código Java neste exemplo, especificamos uma opção de arquivo java_package e um nome para a classe Java no nosso .proto:

option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

Tipos de mensagem

No arquivo routeguide/route_guide.proto do código-fonte, primeiro defina o tipo de mensagem Point. Um Point representa um par de coordenadas de latitude e longitude em um mapa. Neste codelab, use números inteiros para as coordenadas:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Os números 1 e 2 são IDs exclusivos para cada um dos campos na estrutura message.

Em seguida, defina o tipo de mensagem Feature. Um Feature usa um campo string para o nome ou endereço postal de algo em um local especificado por um Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Método de serviço

O arquivo route_guide.proto tem uma estrutura service chamada RouteGuide que define um ou mais métodos fornecidos pelo serviço do aplicativo.

Adicione o método rpc GetFeature à definição RouteGuide. Como explicado anteriormente, esse método vai pesquisar o nome ou endereço de um local em um determinado conjunto de coordenadas. Portanto, faça com que GetFeature retorne um Feature para um determinado Point:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Esse é um método RPC unário: um RPC simples em que o cliente envia uma solicitação ao servidor e aguarda uma resposta, assim como uma chamada de função local.

4. Gerar código de cliente e servidor

Em seguida, precisamos gerar as interfaces do cliente e do servidor gRPC com base na definição do serviço .proto. Para isso, usamos o compilador de buffer de protocolo protoc com um plug-in especial do gRPC Java. É necessário usar o compilador proto3 (que oferece suporte à sintaxe proto2 e proto3) para gerar serviços gRPC.

Ao usar o Gradle ou o Maven, o plug-in de build protoc pode gerar o código necessário como parte do build. Consulte o README do grpc-java para saber como gerar código dos seus próprios arquivos .proto.

Fornecemos um ambiente e uma configuração do Gradle no código-fonte do codelab para criar esse projeto.

No diretório grpc-java-getting-started, execute o seguinte comando:

$ chmod +x gradlew
$ ./gradlew generateProto

As seguintes classes são geradas da nossa definição de serviço:

  • Feature.java, Point.java e outros que contêm todo o código do buffer de protocolo para preencher, serializar e recuperar nossos tipos de mensagens de solicitação e resposta.
  • RouteGuideGrpc.java, que contém (junto com outros códigos úteis) uma classe base para os servidores RouteGuide implementarem, RouteGuideGrpc.RouteGuideImplBase, com todos os métodos definidos no serviço RouteGuide e classes de stub para uso dos clientes.

5. Implementar o servidor

Primeiro, vamos ver como criar um servidor RouteGuide. Há duas partes para fazer nosso serviço RouteGuide funcionar:

  • Implementar a interface de serviço gerada na definição de serviço, que faz o "trabalho" real do serviço.
  • Executar um servidor gRPC para detectar solicitações de clientes e enviá-las à implementação de serviço correta.

Implementar o RouteGuide

Como você pode ver, nosso servidor tem uma classe RouteGuideService que estende a classe abstrata RouteGuideGrpc.RouteGuideImplBase gerada:

private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}

Fornecemos os dois arquivos a seguir para inicializar seu servidor com recursos:

./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java

./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json

Vamos analisar uma implementação simples de RPC em detalhes.

RPC unária

RouteGuideService implementa todos os nossos métodos de serviço. Nesse caso, é apenas GetFeature(), que recebe uma mensagem Point do cliente e retorna em uma mensagem Feature as informações de local correspondentes de uma lista de lugares conhecidos.

@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
  responseObserver.onNext(checkFeature(request));
  responseObserver.onCompleted();
}

O método getFeature() usa dois parâmetros:

  • Point: a solicitação.
  • StreamObserver<Feature>: um observador de resposta, que é uma interface especial para o servidor chamar com a resposta dele.

Para retornar nossa resposta ao cliente e concluir a chamada:

  1. Construímos e preenchemos um objeto de resposta Feature para retornar ao cliente, conforme especificado na definição do serviço. Neste exemplo, fazemos isso em um método checkFeature() particular separado.
  2. Usamos o método onNext() do observador de resposta para retornar o Feature.
  3. Usamos o método onCompleted() do observador de resposta para especificar que terminamos de lidar com a RPC.

Iniciar o servidor

Depois de implementar todos os métodos de serviço, precisamos iniciar um servidor gRPC para que os clientes possam usar nosso serviço. Incluímos no nosso boilerplate a criação do objeto ServerBuilder:

ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)

Vamos criar o serviço no construtor:

  1. Especifique a porta que queremos usar para detectar solicitações do cliente usando o método forPort() do builder (ele usará o endereço curinga).
  2. Crie uma instância da classe de implementação do serviço RouteGuideService e transmita-a ao método addService() do builder.
  3. Chame build() no builder para criar um servidor RPC para nosso serviço.

O snippet a seguir mostra como criar um objeto ServerBuilder.

/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
    this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
        port, RouteGuideUtil.parseFeatures(featureFile));
  }

O snippet a seguir mostra como criar um objeto de servidor para nosso serviço RouteGuide.

/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
  this.port = port;
  server = serverBuilder.addService(new RouteGuideService(features))
      .build();
}

Implemente um método de início que chame start no servidor criado acima.

public void start() throws IOException {
  server.start();
  logger.info("Server started, listening on " + port);
}

Implemente um método para aguardar a conclusão do servidor para que ele não seja encerrado imediatamente.

/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
  if (server != null) {
    server.awaitTermination();
  }
}

Como você pode ver, criamos e iniciamos nosso servidor usando um ServerBuilder.

No método principal, fazemos o seguinte:

  1. Crie uma instância RouteGuideServer.
  2. Chame start() para ativar um servidor RPC para nosso serviço.
  3. Aguarde a interrupção do serviço chamando blockUntilShutdown().
 public static void main(String[] args) throws Exception {
    RouteGuideServer server = new RouteGuideServer(8980);
    server.start();
    server.blockUntilShutdown();
  }

6. Criar o cliente

Nesta seção, vamos criar um cliente para nosso serviço RouteGuide.

Instanciar um stub

Para chamar métodos de serviço, primeiro precisamos criar um stub. Existem dois tipos de stubs, mas só precisamos usar o de bloqueio para este codelab. Os dois tipos são:

  • um stub de bloqueio/síncrono que faz uma chamada de RPC e aguarda a resposta do servidor, retornando uma resposta ou gerando uma exceção.
  • um stub não bloqueador/assíncrono que faz chamadas não bloqueadoras para o servidor, em que a resposta é retornada de forma assíncrona. Você só pode fazer alguns tipos de chamadas de streaming usando o stub assíncrono.

Primeiro, precisamos criar um canal gRPC e usá-lo para criar nosso stub.

Poderíamos ter usado um ManagedChannelBuilder diretamente para criar o canal.

ManagedChannelBuilder.forAddress(host, port).usePlaintext().build

Mas vamos usar um método utilitário que usa uma string com hostname:port.

Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();

Agora podemos usar o canal para criar nosso stub de bloqueio. Neste codelab, temos apenas RPCs de bloqueio. Por isso, usamos o método newBlockingStub fornecido na classe RouteGuideGrpc que geramos do nosso .proto.

blockingStub = RouteGuideGrpc.newBlockingStub(channel);

Chamar métodos de serviço

Agora vamos ver como chamamos os métodos do serviço.

RPC simples

Chamar o RPC simples GetFeature é quase tão simples quanto chamar um método local.

Criamos e preenchemos um objeto de buffer do protocolo de solicitação (no nosso caso, Point), transmitimos para o método getFeature() no nosso stub de bloqueio e recebemos um Feature.

Se ocorrer um erro, ele será codificado como um Status, que pode ser obtido do StatusRuntimeException.

Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();

Feature feature;
try {
  feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
  logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
  return;
}

O boilerplate registra uma mensagem com o conteúdo com base na presença ou não de um recurso no ponto especificado.

7. Faça um teste

  1. No diretório start_here, execute o seguinte comando:
$ ./gradlew installDist

Isso vai compilar seu código, empacotá-lo em um jar e criar os scripts que executam o exemplo. Eles serão criados no diretório build/install/start_here/bin/. Os scripts são: route-guide-server e route-guide-client.

O servidor precisa estar em execução antes de iniciar o cliente.

  1. Execute o servidor:
$ ./build/install/start_here/bin/route-guide-server
  1. Execute o cliente:
$ ./build/install/start_here/bin/route-guide-client

Você vai ver uma saída como esta, com carimbos de data/hora omitidos para maior clareza:

INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906
INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619
INFO: *** GetFeature: lat=0 lon=0
INFO: Found no feature at 0, 0

8. A seguir