RPC: gRPC
gRPC é um framework para invocação remota de procedimentos multi-linguagem e sistema operacional, usando internamente pelo Google há vários anos para implementar sua arquitetura de micro-serviços. Inicialmente desenvolvido pelo Google, o gRPC é hoje de código livre encubado pela Cloud Native Computing Foundation.
O sítio gRPC.io documenta muito bem o gRPC, inclusive os princípios que nortearam seu projeto.
O seu uso segue, em linhas gerais, o modelo discutido nas seções anteriores, isto é, inicia-se pela definição de estruturas de dados e serviços, "compila-se" a definição para gerar stubs na linguagem desejada, e compila-se os stubs juntamente com os códigos cliente e servidor para gerar os binários correspondentes. Vejamos a seguir um tutorial passo a passo, em Java, baseado no quickstart guide.
Instalação
Os procedimentos de instalação dependem da linguagem em que pretende usar o gRPC, tanto para cliente quanto para servidor. No caso do Java, não há instalação propriamente dita.
Exemplo Java
Observe que o repositório base apontado no tutorial serve de exemplo para diversas linguagens e diversos serviços, então sua estrutura é meio complicada. Nós nos focaremos aqui no exemplo mais simples, uma espécie de "hello word" do RPC.
Pegando o código
Para usar os exemplos, você precisa clonar o repositório com o tutorial, usando o comando a seguir.
1 |
|
Uma vez clonado, entre na pasta de exemplo do Java e certifique-se que está na versão 1.42.1, usada neste tutorial.
1 2 |
|
Compilando e executando
O projeto usa gradle para gerenciar as dependências. Para, use o wrapper do gradle como se segue.
1 2 |
|
Proxy
Caso esteja na UFU, coloque também informação sobre o proxy no comando.
1 |
|
Como quando usamos sockets diretamente, para usar o serviço definido neste exemplo, primeiros temos que executar o servidor.
1 |
|
Agora, em um terminal distinto e a partir da mesma localização, execute o cliente, quantas vezes quiser.
1 |
|
O serviço
O exemplo não é muito excitante, pois tudo o que o serviço faz é enviar uma saudação aos clientes.
O serviço é definido no seguinte arquivo .proto
, localizado em ./src/main/proto/helloworld.proto
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
No arquivo, inicialmente são definidas duas mensagens, usadas como requisição (cliente para servidor) e outra como resposta (servidor para cliente) do serviço definido em seguida.
A mensagem HelloRequest
tem apenas um campo denominado name
, do tipo string
. Esta mensagem conterá o nome do cliente, usado na resposta gerada pelo servidor.
A mensagem HelloReply
também tem um campo do tipo string
, denominado message
, que conterá a resposta do servidor.
O serviço disponível é definido pela palavra chave service
e de nome Greeter
; é importante entender que este nome será usado em todo o código gerado pelo compilador gRPC e que se for mudado, todas as referências ao código gerado devem ser atualizadas.
O serviço possui apenas uma operação, SayHello
, que recebe como entrada uma mensagem HelloRequest
e gera como resposta uma mensagem HelloReply
.
Caso a operação precisasse de mais do que o conteúdo de name
para executar, a mensagem HelloRequest
deveria ser estendida, pois não há passar mais de uma mensagem para a operação.
Por outro lado, embora seja possível passar zero mensagens, esta não é uma prática recomendada.
Isto porquê caso o serviço precisasse ser modificado no futuro, embora seja possível estender uma mensagem, não é possível modificar a assinatura do serviço.
Assim, caso não haja a necessidade de se passar qualquer informação para a operação, recomenda-se que seja usada uma mensagem de entrada vazia, que poderia ser estendida no futuro.
O mesmo se aplica ao resultado da operação.
Observe também que embora o serviço de exemplo tenha apenas uma operação, poderia ter múltiplas.
Por exemplo, para definir uma versão em português da operação SayHello
, podemos fazer da seguinte forma.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Observe que a nova operação recebe como entrada mensagens OlaRequest
e OlaReply
, que tem definições exatamente iguais a HellorRequest
e HelloReply
.
Logo, em vez de definir novas mensagens, poderíamos ter usado as já definidas. Novamente, esta não é uma boa prática, pois caso fosse necessário evoluir uma das operações para atender a novos requisitos e estender suas mensagens, não será necessário tocar o restante do serviço.
Apenas reforçando, é boa prática definir requests e responses para cada método, a não ser que não haja dúvida de que serão para sempre iguais.
Implementando um serviço
Agora modifique o arquivo .proto
como acima, para incluir a operação DigaOla
, recompile e reexecute o serviço.
Não dá certo, não é mesmo? Isto porquê você adicionou a definição de uma nova operação, mas não incluiu o código para implementá-la.
Façamos então a modificação do código, começando por ./src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java
.
Este arquivo define a classe que implementa o serviço Greeter
, GreeterImpl
, com um método para cada uma das operações definidas.
Para confirmar, procure por sayHello
para encontrar a implementação de SayHello
; observe que a diferença do casing
vem das boas práticas de Java, de definir métodos e variáveis em Camel casing.
Para que sua versão estendida do serviço Greeter
funcione, defina um método correspondendo à DigaOla
, sem consultar o código exemplo abaixo, mas usando o código de sayHello
como base; não se importe por enquanto com os métodos sendo invocados.
Note que os ...
indicam que parte do código, que não sofreu modificações, foi omitido.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Se você recompilar e reexecutar o código, não perceberá qualquer mudança na saída do programa. Isto porquê embora tenha definido um novo serviço, você não o utilizou. Para tanto, agora modifique o cliente, em src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java
, novamente se baseando no código existente e não se preocupando com "detalhes".
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Agora sim, você pode reexecutar cliente e servidor.
1 2 3 |
|
Percebeu como foi fácil adicionar uma operação ao serviço? Agora nos foquemos nos detalhes, começando sobre como um servidor gRPC é criado.
Observe que um objeto Server
é criado por uma fábrica que recebe como parâmetros a porta em que o serviço deverá escutar e o objeto que efetivamente implementa as operações definidas no arquivo .proto
. O start()
também é invocado na sequência e, estudando o código, você entenderá como o fim da execução é tratada.
1 2 3 4 5 6 7 8 9 |
|
Do lado do cliente, é criado um ManagedChannel
e com este um GreeterBlockingStub
, um stub em cujas chamadas são bloqueantes.
Finalmente, no stub são invocados os serviços definidos na IDL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Diga Olás!
Para fixar o conteúdo é preciso colocar a mão na massa. Estenda a definição do serviço com uma operação DigaOlas
em que uma lista de nomes é enviada ao servidor e tal que o servidor responda com **uma longa string**cumprimentando todos os nomes, um após o outro.
Só abra depois de pensar em como resolver o problema
Você pode usar repeated
no campo message
do tipo HelloRequest
.
Stream
Para terminar este estudo de caso, modifique a função definida no exercício anterior para gerar múltiplas respostas, uma para cada nome passado, em vez de uma única, longa, resposta.
Só abra depois de pensar em como resolver o problema
Você deverá usar streams.
1 |
|
- Do lado do servidor
1 2 3 4 5 6 7 8 9 10 11
List<String> listOfHi = Arrays.asList("e aih", "ola", "ciao", "bao", "howdy", "s'up"); @Override public void digaOlas(OlaRequest req, StreamObserver<OlaReply> responseObserver) { for (String hi: listOfHi) { OlaReply reply = OlaReply.newBuilder().setMessage(hi + ", " req.getName()).build(); responseObserver.onNext(reply); } responseObserver.onCompleted(); }
- Do lado do cliente
1 2 3 4 5 6 7 8 9 10 11
OlaRequest request = OlaRequest.newBuilder().setName(name).build(); try { Iterator<OlaReply> it = blockingStub.digaOlas(request); while (it.hasNext()){ OlaReply response = it.next(); logger.info("Greeting: " + response.getMessage()); } } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); return; }
Desafio de Interoperabilidade
Siga o tutorial abaixo e execute use o gRPC em Python. Uma vez executados cliente e servidor, tente fazer com que interaja com a implementação em java.
1 2 3 4 5 6 7 8 9 10 |
|
Para recompilar os stubs, faça
1 |
|
Modifique o servidor
1 2 |
|
Modifique o cliente
1 2 |
|
Outros modos de trabalho
gRPC é um framework bem flexível e para entender como é possível usá-lo para estabelecer canais de comunicação para fluxos, além da documentação já apontada, sugiro o artigo gRPC: A Deep Dive into the Communication Pattern, que a detalha.