Skip to content

7. Desenvolvimento

Eder Marques edited this page Jan 2, 2023 · 43 revisions

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.

Esboço da arquitetura implementada
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.

7.1. JavaScript – Sorteador de número

A API em JavaScript irá oferecer um serviço de sorteio de número inteiro dentro de um intervalo fechado.

7.1.1. Definição de contrato – sorteio.proto

Primeiramente, precisamos da definição do contrato comum entre o servidor JS e os seus clientes - sorteio.proto, da Figura 4.

sorteio.proto
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.

7.1.2. Servidor gRPC – NodeJS

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.

server.js
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.

Implementação do método sortearNumero
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).

Colocando o servidor no ar
Figura 7 - Colocando o servidor no ar


⬆️ Ir ao topo



7.2. Golang – Fornecedor de id

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.

7.2.1. Definição de contrato – gerador_id.proto

Na Figura 8 temos a definição de contrato gerador_id.proto.

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.

Compilando .proto Go através do comando protoc
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.

Arquivos gerados após compilação protoc Go
Figura 10 - Arquivos gerados após compilação protoc Go

7.2.2. Servidor gRPC – Golang

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.

Implementação Go
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.

Método main em Go
Figura 12 - Método main do servidor Golang


⬆️ Ir ao topo



7.3. Dart – Banco de dados

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.

7.3.1. Definição de contrato – 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.

aluno.proto
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.

Compilando .proto Dart através do comando protoc
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.

Arquivos gerados após compilação protoc Dart
Figura 15 - Arquivos gerados após compilação protoc Dart

7.3.2. Servidor gRPC de banco de dados e Cliente gRPC de Golang

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.

Estendendo classe abstrata CrudAlunoService
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.

Operação de inserção no banco
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.

Demais métodos da classe CrudAlunoService
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).

Disponibilidade de serviço Dart para chamadas remotas
Figura 19 - Disponibilidade de serviço Dart para chamadas remotas


⬆️ Ir ao topo



7.4. Java – Aplicação Cliente

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.

Estrutura de pacotes 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.

Classe AgendaContatos
Figura 21 - Classe AgendaContatos

Classe Intervalo
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.

Criação de Aluno
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.

Métodos para criar Aluno:  sortearNomePessoa e getNumeroSorteado
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.

Método createAluno cliente da API Dart
Figura 25 - Método createAluno cliente da API Dart


⬆️ Ir ao topo



7.5. Simulação

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.

Ambiente de Execução
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.

Servidor e cliente Go
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.

Listagem de alunos: parte da saída da aplicação Java
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.

Servidor NodeJS servindo id 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.

Mensagens de log do servidor Dart
Figura 30 - Mensagens de log do servidor Dart


⬆️ Ir ao topo



Clone this wiki locally