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
GetFeatureque o servidor implementa e o cliente chama. - Os tipos de mensagem
PointeFeature, que são estruturas de dados trocadas entre o cliente e o servidor ao usar o métodoGetFeature. O cliente fornece coordenadas do mapa como umPointna solicitaçãoGetFeatureao servidor, e o servidor responde com umFeaturecorrespondente 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.javae 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 servidoresRouteGuideimplementarem,RouteGuideGrpc.RouteGuideImplBase, com todos os métodos definidos no serviçoRouteGuidee 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:
- Construímos e preenchemos um objeto de resposta
Featurepara retornar ao cliente, conforme especificado na definição do serviço. Neste exemplo, fazemos isso em um métodocheckFeature()particular separado. - Usamos o método
onNext()do observador de resposta para retornar oFeature. - 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:
- 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). - Crie uma instância da classe de implementação do serviço
RouteGuideServicee transmita-a ao métodoaddService()do builder. - 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:
- Crie uma instância
RouteGuideServer. - Chame
start()para ativar um servidor RPC para nosso serviço. - 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
- 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.
- Execute o servidor:
$ ./build/install/start_here/bin/route-guide-server
- 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
- Saiba como o gRPC funciona em Introdução ao gRPC e Conceitos principais.
- Siga as etapas do tutorial de noções básicas.
- Confira a referência da API.