-
Notifications
You must be signed in to change notification settings - Fork 0
7. Desenvolvimento
Para o estudo prático nós construímos um sistema distribuído de microsserviços fazendo a integração de sistemas implementados em quatro linguagens de programação diferentes, utilizando para a comunicação e interoperabilidade dos sistemas o gRPC.
Na Figura 3 vemos a arquitetura implementada. A aplicação Java precisa criar uma lista de 10 alunos amostrados aleatoriamente de uma lista interna de 51. Para isso, ele consome uma API que oferece o serviço de sortear números inteiros dentro dos limites de um intervalo. A aplicação Java fornece um intervalo de 0 a 50 e fica fazendo chamadas remotas ao servidor NodeJS até preencher a quantidade desejada.
Figura 3 - Esboço da arquitetura implementada
Depois precisa persistir os alunos selecionados em um banco de dados. Uma API implementada em Dart fará a emulação de um banco de dados oferecendo um serviço de API contendo todos os métodos das operações elementares de um banco: criar, atualizar, buscar por id
, listar todos e apagar. O serviço Dart salva os dados apenas em memória.
Para emular a inserção em um banco, usualmente cria-se um número sequencial para o id. Para testarmos a comunicação entre as APIs, a API Dart é cliente de outra API escrita em Golang. A API Golang faz o papel de gerador de chave primária, fornecendo números sequências a cada chamada remota, para que o Dart possa usá-lo como identificador único da entidade Aluno, que a aplicação Java lhe solicitou para salvar no banco.
Desta forma, temos quatro sistemas implementados em linguagens distintas se comunicando, fornecendo e consumindo serviços de API com o gRPC.
A API em JavaScript irá oferecer um serviço de sorteio de número inteiro dentro de um intervalo fechado.
Primeiramente, precisamos da definição do contrato comum entre o servidor JS e os seus clientes - sorteio.proto
, da Figura 4.
Figura 4 - sorteio.proto
Na linha 3 estamos informando ao plugin-compilador protobuffer que estamos usando a linguagem de definição na versão 3. A palavra service
denota um serviço em gRPC, logo, na linha 5 estamos declarando o serviço SorteioService
. Tal como métodos e classe, em que as chamadas aos métodos são realizadas através de instâncias de classe, para invocarmos um método gRPC, ele precisa estar referenciado em um serviço. Podemos declarar vários serviços em um arquivo .proto
.
Os métodos são declarados dentro do escopo do serviço com a palavra reservada rpc
. Ela informa ao compilador que o método é um endpoint que ficará disponível às chamadas de clientes remotos. Em nosso sorteio.proto
, nós temos apenas o método SortearNumero
dentro do serviço SorteioService
. SortearNumero
recebe como argumento um tipo de dado chamado IntervaloRequest
e retorna o tipo de dado SorteadoResponse
. A sua implementação deverá retornar um número inteiro dentro de um intervalo de domínio.
Os tipos de dados são idealizados como tipos de mensagens, denotados pela palavra reservada message
. Cada tipo message
possui campos ou atributos e cada atributo possui um identificador único numérico. Essa numeração sequencial dos atributos é utilizada pelo framework gRPC para fazer a serialização e desserialização na ordem correta.
No tipo de mensagem IntervaloRequest
da linha 9, temos dois atributos de tipo inteiro em sua estrutura de dado. Nos atributos min
e max
estarão os limites inferior e superior, respectivamente, com ambos os limites inclusos, dentro dos quais um número deverá ser sorteado. O tipo de mensagem SorteadoResponse
é o tipo de dado que a chamada remota ao método retornará, armazenado em seu campo numero
o número sorteado.
A principal ferramenta de manipulação de protofiles é o protoc
, o gerador de códigos e parser do protobuf, que compila os arquivos de definição de contrato .proto
em arquivos .pb.js
. Entretanto, com JavaScript vamos gerar o conteúdo dinamicamente com @grpc/proto-loader
. Ao invés do arquivo ser pré-compilado, com esta ferramenta o arquivo .proto
é carregado em memória e "parseado" em tempo de execução.
Figura 5 - server.js
Inicialmente criamos uma representação do arquivo .proto
na linha 6 e na linha 7 fornecemos esse objeto para o gRPC para que ele crie uma definição válida, com a qual poderemos criar um serviço de API na linha 8. Na linha 10 instanciamos o servidor e na linha 11 adicionamos o serviço, fornecendo dois parâmetros, o serviço e a lista de funções que implementam os métodos declarados na definição de contrato do .proto
, que no nosso caso temos apenas um, sortearNumero
. Na Figura 6 temos a implementação do método.
Figura 6 - Implementação do método sortearNumero
Por fim, colocamos o servidor no ar, ouvindo na porta 50053 com autenticação vazia para o HTTP/2 (Figura 7).
Figura 7 - Colocando o servidor no ar
A API em Golang será o provedor de id numérico. A ideia é que o Go fará as vezes do gerador de chave primária numérica sequencial de um sistema gerenciador de bancos de dados. O banco de dados será emulado na linguagem Dart, tratado no capítulo seguinte.
Na Figura 8 temos a definição de contrato gerador_id.proto
.
Figura 8 - Definição de contrato gerador_id.proto
Na definição de contrato em sorteio.proto
do JS, procuramos apresentar os elementos estritamente necessários de um arquivo .proto
– a especificação da sintaxe, as declarações de métodos e serviço e os tipos de dados de mensagem. Já neste capítulo, em gerador_id.proto
da Figura 8, apresentamos outros elementos da linguagem de definição do protobuf. Uma declaração de pacote na linha 8, uma declaração opcional específica da linguagem Golang na linha 6, que diz respeito a organização de pastas e arquivos do projeto no GitHub, e uma declaração de importação de tipo de mensagem externa na linha 4.
A API do Go possui apenas o serviço GeradorID
declarado na linha 10, que por sua vez possui apenas um método, GerarID
. O método GerarID
está declarado na linha 11 e recebe uma mensagem Empty
e retorna um objeto message
do tipo IDReply
que contém em sua estrutura o atributo inteiro goId
contendo o dado requerido.
A implementação do método GerarID
não terá argumento, não receberá nenhum parâmetro, entretanto, na sua definição protobuf faz-se necessário especificar um tipo de mensagem vazia, equivalente ao void
de outras linguagens, algo como message Empty {}
.
Na API Dart tratada mais adiante, há também a necessidade desse tipo de mensagem em dois métodos em seu arquivo aluno.proto
, no método GetAllAlunos
, cuja implementação não recebe parâmetros bem como no método DeleteAluno
, que não retorna nada. Sendo assim, teríamos duas declarações de Empty
distintas, uma em aluno.proto
e outra em gerador_id.proto
. Como efeito, o Dart não saberia a qual classe estaríamos nos referindo. Este conflito poderia ser resolvido fornecendo o nome da classe completamente qualificado, mas optamos por enriquecer nosso exemplo fazendo a importação de uma declaração de Empty
referenciada em um pacote comum de domínio público (linha 4), o que facilita a distribuição de definições de tipos de mensagens.
Uma vez definida a interface no arquivo .proto
, precisamos compila-lo. Para isso, invocamos protoc
com o plugin do Go digitando em um terminal shell a linha de comando da Figura 9.
Figura 9 - Compilando .proto Go através do comando protoc
Podemos observar os dois aquivos .proto
utilizados pela API, o local gerador_id.pronto
e o que fora referenciado externamente, o empty.proto
. Como resultado de sua execução, teremos os arquivos .pb.go
gerados pelo plugin gRPC e dentro deles as classes stubs.
Figura 10 - Arquivos gerados após compilação protoc Go
A implementação Go mostrada da Figura 11 começa com importações básicas típicas e do pacote gRPC, bem como os pacotes contendo as classes stubs geradas pelo compilador, uma que atribuímos o alias emptypb
para o pacote externo do google e pb
para o local. Na sequência, temos uma constante para a porta e na linha 16 a variável id
que será incrementada a cada chamada.
Figura 11 - Implementação do servidor Golang
Nas linhas 18 a 20 temos o stub do servidor que atenderá as requisições. Nas linhas 22 a 26, a implementação do método GerarID
, que recebe o contexto e um objeto Empty
, e retorna um objeto IdReply
(linha 25) contendo o valor do id
, após realizado seu incremento na linha 23.
Por fim, temos o método main
, no qual abrimos o canal gRPC criando uma conexão de rede com protocolo de transporte TCP, ouvindo as requisições na porta definida na constante port
. Na linha 35 criamos o servidor e na linha 36 o iniciamos.
Figura 12 - Método main do servidor Golang
A API em Dart irá emular o banco de dados. No relacionamento com o Java, será o servidor do banco de dados, enquanto o Java ficará do lado cliente. Porém, em relação ao Golang, o Dart estará do lado cliente e o Golang do lado servidor, pois o Golang fará o papel de gerador de chave primária sequencial. O Dart trabalha com três arquivos .proto
. O gerador_id.proto
para poder consumir os serviços da API em Go. O arquivo aluno.proto
para o qual proverá as implementações de servidor para a aplicação cliente Java. E por último, o arquivo empty.proto
, que vem de um pacote externo para servir de definição de tipo message
comum em aluno.proto
e em gerador_id.proto
. Destes três, dois já foram apresentados, só nos falta aluno.proto
.
Na Figura 13 a seguir temos o arquivo aluno.proto
, onde podemos ver no seu início a especificação da sintaxe e a importação do tipo de message
Empty
de um pacote externo. Na linha 6 temos a declaração do serviço da API, CrudAlunoService
, e dentro do escopo do serviço, os métodos que representam as operações básicos de um banco de dados. No nosso caso, neste banco emulado (mock), temos apena uma tabela, a entidade Aluno
.
Figura 13 - aluno.proto
O método createAluno
recebe como parâmetro o tipo Aluno
, sem chave primária (id) e retorna o objeto (message
) Aluno
persistido, já com id. O método EditAluno
é usado para atualizar um aluno já persistido e, tal qual createAluno
, recebe o tipo Aluno
como argumento e retorna o mesmo tipo, Aluno
. Vemos na definição do tipo Aluno
(linha 18) que este possui dois atributos, um inteiro (id
) e outro do tipo string (nome
).
GetAllAlunos
recebe uma mensagem do tipo Empty
e retorna uma coleção (Alunos
) de tipo Aluno
. Interessante notar como o protobuf declara coleções de forma simples. A coleção Alunos
está declarada na linha 23 e na linha 24 vemos pela primeira vez a palavra-chave repeated
, usada para denotar coleção do tipo Aluno
. O método GetAluno
recebe um outro tipo de mensagem, AlunoId
, que possui internamente o atributo id
e retorna um Aluno
. DeleteAluno
recebe o mesmo tipo de parâmetro do método GetAluno
, porém, retorna um objeto vazio, Empty
.
Uma vez que a definição esteja pronta, precisamos compilar os arquivos .proto
para que o framework gRPC gere as classes necessárias à abstração da comunicação pelo protocolo HTTP/2.
Figura 14 - Compilando .proto Dart através do comando protoc
Na Figura 14 invocamos o compilador protoc
; com -I=protos/
informamos onde estão os arquivos com extensão .proto
, com –dart_out=grpc:protos/
estamos dizendo para o protoc
compilar usando o plugin do Dart e para descarregar os arquivos gerados na mesma pasta que estão os .proto
, e por fim, a lista dos arquivos que devem ser compilados.
Após a execução, a pasta protos/
que continha apenas os dois arquivos .proto
locais, passa a ter mais nove arquivos, três deles em uma nova pasta referente ao empty.proto
externo, como podemos ver na Figura 15.
Figura 15 - Arquivos gerados após compilação protoc Dart
Para implementarmos o servidor de banco de dados Dart nós estendemos a classe abstrata CrudAlunoServiceBase
, encontrada no arquivo aluno.pbgrp.dart
, gerado pelo framework gRPC na compilação. Vemos com mais clareza os conceitos elementares da comunicação do gRPC, canal e stub. O compilador criou uma classe que abstrai o canal de comunicação, ClientChannel
e a classe GeradorIDClient
que é o stub.
Nos foi gerado também a classe Alunos
, a partir da qual instanciamos essa coleção no objeto lista
, que será a tabela do nosso banco emulado.
Figura 16 - Estendendo classe abstrata CrudAlunoService
O único momento em que se precisa gerar uma chave primária é na operação de inserção no banco. Portanto, apenas o método createAluno
fará uso da API do Go.
Figura 17 - Operação de inserção no banco
No corpo do método createAluno
nós declaramos uma variável inteira, id
, e verificamos se o valor do id
foi fornecido no parâmetro request
. Caso esteja presente, usaremos o que foi enviado, caso contrário usaremos a API Go para gerar o id para nós. Em seguida terminamos de preencher o objeto aluno
e o adicionamos à lista
(o equivalente a inserir na tabela).
Para consumirmos a API Go, primeiramente estabelecemos o canal de comunicação entre o nosso código Dart e a API, passando como argumento o endereço ip (localhost), a porta e as opções do canal. Assim como no JavaScript, nas opções do canal passamos um objeto de credenciais vazias (linhas 19 a 22). Em seguida, na linha 23 instanciamos o stub, o objeto que corresponde ao serviço exposto pelo servidor remoto, fornecendo o canal recém-criado, com a opção de aguardar por trinta segundo uma resposta às requisições. Usando um bloco try-catch
invocamos o método desejado através do stub.
Os demais métodos do serviço CrudAlunoService
não consomem API, apenas manipulam os elementos da lista (Figura 18). Podemos notar que todos os métodos possuem um ServiceCall
, esse objeto call
contém metainformações do request.
Figura 18 - Demais métodos da classe CrudAlunoService
Vimos como o Dart consome uma API gRPC, a seguir, veremos como o Dart disponibiliza seu serviço para chamadas remotas. Na Figura 19 nós temos o método main
, onde instanciamos o servidor, passando ao construtor a instância do serviço que queremos oferecer, CrudAlunoService
(linhas 87 e 88). E por fim, iniciamos o servidor na porta 50052 (linha 91).
Figura 19 - Disponibilidade de serviço Dart para chamadas remotas
O Java será aplicação cliente de duas API’s, da API JavaScript que oferece o serviço de sorteio de número inteiro dentro de um intervalo entre mínimo e máximo, e da API Dart que oferece serviços de banco de dados. Para o Java, mudamos de editor e usamos a IDE Eclipse, muito popular na comunidade. No Eclipse criamos um projeto Maven que irá gerir as dependências e compilar os arquivos .proto
. Na Figura 20 vemos a estrutura de pacotes do projeto Java.
Figura 20 - Estrutura de pacotes projeto Java
A aplicação Java consome duas API e os seus arquivos protobuf estão no pacote resources
. Com sorteio.proto
a aplicação se comunica com a API NodeJS e aluno.proto
com a API Dart. Para compilar os arquivos .proto
, executamos dentro do próprio Eclipse o Maven-build e os arquivos gerados vão para o pacote java_grpc
. Vemos dentro do pacote que foram gerados dois arquivos para cada .proto
.
No pacote model
temos duas classes. AgendaContatos
possui internamente uma lista de nomes de 51 elementos e um método para obtê-los a partir do índice. A classe Intervalo
possui os atributos inteiros min
e max
e será usada na chamada da API JavaScript. A listagem das classes AgendaContatos
e Intervalo
seguem as Figuras 21 e 22, respectivamente.
Figura 21 - Classe AgendaContatos
Figura 22 - Classe Intervalo
Nós implementamos testes para todos os métodos definidos nos serviços, mas vamos focar na criação de Aluno
, pois utiliza as duas APIs. Na linha 129 da Figura 23 obtemos um nome para o aluno invocando o método sortearNomePessoa
, passando como parâmetro um objeto Intervalo
.
Figura 23 - Criação de Aluno
Na linha 27 da Figura 24 vemos que o método sortearNomePessoa
obtém o nome da pessoa do objeto contatos
, passando para ele o índice da sua lista de contato. Esse índice vem de getNumeroSorteado
que é o método cliente da API JavaScript.
Figura 24 - Métodos para criar Aluno: sortearNomePessoa e getNumeroSorteado
No corpo do método getNumeroSorteado
criamos o canal de comunicação passando o endereço e a porta. Em seguida conseguimos o stub do serviço SorteioService
passando o canal a um método construtor. O Java adotou o padrão de projeto Builder para a instanciação de objetos. Dessa forma, construímos o objeto do IntervaloRequest
que é passado ao stub para fazer a chamada remota ao servidor NodeJS.
Retomando à Figura 23, construímos o objeto alunoToCreate
na linha 132 e na 134 chamamos o método cliente da API Dart, createAluno
, lista
do na Figura 25. A chamada remota ao microserviço em Dart se dá de maneira idêntica à do microserviço em JavaScript, criamos o canal, o usamos para construir o stub e por meio deste fazemos a chamada.
Figura 25 - Método createAluno cliente da API Dart
Na figura 26 vemos o nosso ambiente de execução de testes. Temos do lado direto o Eclipse executando o Java com o Console expandido, e à esquerda uma grande janela de terminal que foi multiplexada com o tmux em três coluna. A coluna mais a esquerda é dedicada ao Golang, a do meio ao servidor NodeJS e a da direita executa o servidor de banco dados Dart.
Figura 26 - Ambiente de Execução
Na figura 27 temos em destaque a coluna do Golang e podemos ver que ela está dividida em dois shells, no superior temos o servidor de id ouvindo na porta 50051, e no shell de baixo executamos por seis vezes um cliente Golang, que com efeito, fez o servidor Golang gerar os seis primeiros ids. Em consequência, quando executarmos a API Dart e esta for adicionar um aluno ao banco, e para tanto, precisará solicitar ao servidor Golang o id, a API Dart receberá o id de número sete, pois os seis primeiros já foram gerados.
Figura 27 - Servidor e cliente Golang
Na figura 28 temos um trecho da saída padrão do editor Eclipse, mostrando a lista de dez alunos gerada e persistida no banco de dados Dart. Nesse recorte, a API Java fez uma chamada remota ao método getAllAlunos
da API Dart. Notamos que o id obtido pelo Dart, fornecido pela API Golang, foi mesmo o número sete, em decorrência das requisições anteriores feitas com o cliente Golang.
Figura 28 - Listagem de alunos: parte da saída da aplicação Java
Para compor a listagem, a aplicação Java consumiu a API NodeJS que faz o sorteio de números inteiros dentro de um intervalo. A figura 29 traz um recorte da coluna do meio do terminal, onde vemos o sorteador NodeJS respondendo às requisições na porta 50053.
Figura 29 - Servidor NodeJS servindo id na porta 50053
Na figura 30 temos o início das mensagens de log no terminal do servidor Dart de banco de dados. Vemos que o a API Dart responde na porta 50052 e que seu primeiro aluno inserido possui id igual a sete.
Figura 30 - Mensagens de log do servidor Dart
Éder Marques - @earmarques - [email protected]
All rights reserved - Distributed above GPL3 license. See LICENSE to more information.
-
Resumo
-
1. Introdução
-
2. Justificativa
-
3. Objetivos
-
4. Fundamentação Teórica
4.1. RPC Legado
4.2. REST
4.3. gRPC
4.4. Golang
4.5. Dart
4.6. Protocol Buffers
-
5. Trabalhos Similares
-
6. Metodologia
-
7. Desenvolvimento
7.1. JavaScript - Sorteador de número
7.1.1. Definição de contrato – sorteio.proto
7.1.2. Servidor gRPC – NodeJS
7.2. Golang – Fornecedor de id
7.2.1. Definição de contrato – gerador_id.proto
7.2.2. Servidor gRPC – Golang
7.3. Dart – Banco de dados
7.3.1. Definição de contrato – aluno.proto
7.3.2. Servidor gRPC de banco de dados e Cliente gRPC de Golang
7.4. Java – Aplicação Cliente
7.5. Simulação
-
8. Resultados e Discussões
-
9. Conclusões
-
Referências