Temos as seguintes etapas para fazer:
1. asdf
1.1. Instalar o plugin asdf do Dart
1.2. Instalar uma versão do Dart
1.3. Tornar a versão global
2. Dart
2.1 Plugin Protocol Buffer Compiler
2.2. Atualizar o PATH
2.3. Criar uma aplicação de console
2.4. Adicionar dependências
3. Contratos *.proto
3.1. Arquivos Protobuf
3.2 Compilar os arquivos .proto
4. Códigos cliente e servidor
4.1. Microservice CrudAlunoService
4.1.1. server.dart
4.1.2. client.dart
4.1.3. Teste CRUD banco de dados Dart
4.2. Microservice CrudAlunoService
4.3. Microservice CrudAlunoService com GeradorID
📌 Para uma fundamentação teórica e explicação do código, recomendamos a leitura da Wiki do projeto. Aqui vamos cuidar apenas dos comandos para fazer o gRPC funcionar com JavaScript.
touch protos/aluno.proto
asdf plugin-add dart https://github.com/patoconnor43/asdf-dart.git
asdf install dart 2.17.6
Faremos da 2.17.6 a versão global para todos os projetos na máquina
asdf global dart 2.17.6
Checar instalação
dart --version
> Dart SDK version: 2.17.6 (stable) (Tue Jul 12 12:54:37 2022 +0200) on "linux_x64"
Instalar o plugin do compilador dos arquivos .proto
.
dart pub global activate protoc_plugin
Precisamos atualizar o PATH para que o protoc possa encontrar o plugin que acabamos de instalar.
export PATH="$PATH":"$HOME/.pub-cache/bin"
Estrutura projeto console Dart Este comando modifica temporariamente o PATH, apenas para o shell que estivermos usando. Se quisermos evitar ter de executarmos o comando anterior toda vez que formos compilar arquivos .proto para o Dart, devemos acrescentar essa linha de comando ao finalCódigo Dart gerado a partir das especificações de serviços . do arquivo ~/.bashrc.
Figura 1: Path para o plugin do compilador protobuff no ~/.bashrc
Nosso shell é bash, executar source ~/.bashrc
carrregará as novas configurações. Se usa outro shell, feche e abra o terminal novamente.
Dentro do diretório tcc_grpc
, execute:
dart create --template=console-simple dart_grpc
O comando cria uma aplicação de console no diretório dart_grpc com a estrutura da figura 2.
Figura 2: Estrutura projeto console Dart
Vamos adicionar duas dependências ao arquivo pubspec.yaml: grpc: ^3.1.0
e protobuf: ^2.1.0
.
- Editar
pubspec.yaml
Figura 3: Dependências no pubspec.yaml
- Instalar dependências
dart pub get
Os arquivos .proto de definições de serviços ficarão no diretório protos
.
O Dart consumirá a API Golang, logo, o Dart precisa do mesmo contrato, da mesma interface especificada pelo arquivo LINK TODO -> gerador_id.proto presente no repositório da API Go. A outra definição de serviço é aluno.proto
onde vamos declarar todas as operações elementares do banco de dados.
mkdir -p protos;
cp ../go_grpc/protos/gerador_id.proto protos/ ;
touch protos/aluno.proto
O arquivo aluno.proto
deve estar como na listagem 1.
// aluno.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
service CrudAlunoService {
rpc CreateAluno(Aluno) returns (Aluno);
rpc GetAllAlunos(google.protobuf.Empty) returns (Alunos);
rpc GetAluno(AlunoId) returns (Aluno);
rpc DeleteAluno(AlunoId) returns (google.protobuf.Empty) {};
rpc EditAluno(Aluno) returns (Aluno) {};
}
message AlunoId {
int32 id = 1;
}
message Aluno {
int32 id = 1;
string nome = 2;
}
message Alunos {
repeated Aluno alunos = 1;
}
Listagem 1: aluno.proto
Temos três protobuff para compilar, aluno.proto
, gerador_id.proto
e o empty.proto
, que está importado nos dois primeiros, nos quais o usamos como tipo de message
para procedimentos que não recebem argumentos, como em CrudAlunoService#GetAllAlunos
e GeradorID#GerarId
, ou nos procedimentos que retornam vazio (void
), como em CrudAlunoService#DeleteAluno
.
Na pasta dart_grpc
, compile os arquivos com o comando:
protoc -I=protos/ --dart_out=grpc:protos/ \
protos/aluno.proto \
protos/gerador_id.proto \
google/protobuf/empty.proto
Teremos os vários códigos .dart
gerados da figura 4.
Figura 4: Códigos Dart gerados a partir das especificações de serviços .proto
Vamos fazer uma abordagem cautelosa cartesiana - análise e síntese, conquistando passo a passo cada recurso de que precisamos para depois formarmos o todo. Primeiro vamos fazer um cliente-servidor apenas em Dart do nosso CrudAlunoService
(client.dart
e server.dart
). Depois vamos fazer um cliente Dart consumir o service
Golang GeradorID
(client_id.dart
). Por fim, vamos fazer um cliente-servidor Dart cujo servidor de serviço é por sua vez cliente de outro serviço em Golang (server_cliente_go.dart
e client_id_go.dart
). Estes códigos .dart estarão abrigados na pasta dart_grpc/bin
.
Nosso banco de dados é emulado (mock) com uma lista em memória: Alunos lista = Alunos();
. Em #createAluno
o id
do aluno já deve vir fornecido no objeto request
. Posteriormente esse id será gerado pelo microserviço em Golang.
import 'package:grpc/grpc.dart';
import '../protos/aluno.pbgrpc.dart';
import '../protos/google/protobuf/empty.pb.dart';
class CrudAlunoService extends CrudAlunoServiceBase {
Alunos lista = Alunos();
@override
Future<Aluno> createAluno(ServiceCall call, Aluno request) async {
var aluno = Aluno();
aluno.nome = request.nome;
aluno.id = request.id;
lista.alunos.add(aluno);
return aluno;
}
@override
Future<Alunos> getAllAlunos(ServiceCall call, Empty request) async {
return lista;
}
@override
Future<Aluno> getAluno(ServiceCall call, AlunoId request) async {
var aluno = lista.alunos.firstWhere((aluno) => aluno.id == request.id);
return aluno;
}
@override
Future<Empty> deleteAluno(ServiceCall call, AlunoId request) async {
lista.alunos.removeWhere((aluno) => aluno.id == request.id);
return Empty();
}
@override
Future<Aluno> editAluno(ServiceCall call, Aluno request) async {
var aluno = lista.alunos.firstWhere((aluno) => aluno.id == request.id);
aluno.nome = request.nome;
return aluno;
}
}
Future<void> main(List<String> args) async {
final server = Server(
[CrudAlunoService()],
const <Interceptor>[],
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
);
await server.serve(port: 50052);
print('Servidor ouvindo na porta ${server.port}...');
}
Listagem 2: server.dart
Na classe Client
criamos o canal de comunicação channel
e o objeto stub
que representa no cliente o serviço oferecido pelo servidor, pelo qual faremos as chamadas remotas. Inicialmente criamos dois objetos alunos, passando o id manualmente, e depois testamos todas as remote procedure calls.
import 'package:grpc/grpc.dart';
import './../protos/aluno.pbgrpc.dart';
import '../protos/google/protobuf/empty.pb.dart';
class Client {
late ClientChannel channel;
late CrudAlunoServiceClient stub;
Future<void> main(List<String> args) async {
channel = ClientChannel('localhost',
port: 50052,
options: // Aqui não teremos credenciais
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = CrudAlunoServiceClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
try {
//...
print('\n__ Adicionando Alunos ---------------------------------');
var alunoToAdd1 = Aluno();
alunoToAdd1.id = 1;
alunoToAdd1.nome = "Elias Mantovani Rebouças";
var alunoAdicionado1 = await stub.createAluno(alunoToAdd1);
print("Aluno Adicionado:\n" + alunoAdicionado1.toString());
var alunoToAdd2 = Aluno();
alunoToAdd2.id = 2;
alunoToAdd2.nome = "Pedro Henrique Coimbra";
var alunoAdicionado2 = await stub.createAluno(alunoToAdd2);
print("Aluno Adicionado:\n" + alunoAdicionado2.toString());
print('\n__ Listagem de Alunos ---------------------------------');
var todosAlunos = await stub.getAllAlunos(Empty());
print(todosAlunos.alunos.toString());
print('\n__ Removendo Aluno ------------------------------------');
var alunoToDel = AlunoId();
alunoToDel.id = 1;
await stub.deleteAluno(alunoToDel);
print("Aluno removido com ID: " + alunoToDel.id.toString());
print('\n__ Listagem de Alunos ---------------------------------');
var todosAlunos2 = await stub.getAllAlunos(Empty());
print(todosAlunos2.alunos);
print('\n__ Edição de Aluno ------------------------------------');
var alunoToEdit = Aluno();
alunoToEdit.id = 2;
alunoToEdit.nome = "David Bitcoin";
await stub.editAluno(alunoToEdit);
print("Aluno editado com ID: " + alunoToEdit.id.toString());
print('\n__ Busca do Aluno Editado -----------------------------');
var alunoToGet = AlunoId();
alunoToGet.id = 2;
var alunoObtido = await stub.getAluno(alunoToGet);
print("Aluno de id = 2 com nome editado:\n" + alunoObtido.toString());
print('\n__ Listagem de Alunos ---------------------------------');
var todosAlunos3 = await stub.getAllAlunos(Empty());
print(todosAlunos3.alunos);
}
catch (e) {
print('\n\nErro: O Servidor está offline\n');
print(e);
}
await channel.shutdown();
}
}
main() {
var client = Client();
client.main([]);
}
Listagem 3: client.dart
Precisamos de dois terminais, um para o servidor do banco e outro para o cliente testar. Dentro do diretório bin:
- terminal do servidor
dart server.dart
- terminal do cliente
dart client.dart
Com êxito, teremos o resultado mostrado na figura 5.
O segundo recurso que precisamos é fazer um cliente Dart consumir o microserviço Golang de gerar o índice incremental para ser nossa chave pimária do banco Dart. Para isso vamos escrever apenas o código necessário para se comunicar com a API Golang, client_id.dart
.
import 'package:grpc/grpc.dart';
import './../protos/gerador_id.pbgrpc.dart';
import '../protos/google/protobuf/empty.pb.dart';
class Client {
late ClientChannel channel;
late GeradorIDClient stub;
Future<void> main(List<String> args) async {
channel = ClientChannel('localhost',
port: 50051,
options: // Aqui não teremos credenciais
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = GeradorIDClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
try {
//...
var id = await stub.gerarId(Empty());
print('goId:${id.goId}');
} catch (e) {
print('\n\nErro: O Servidor está offline\n');
print(e);
}
await channel.shutdown();
}
}
main() {
var client = Client();
client.main([]);
}
Listagem 4: client_id.dart
Agora em um shell vamos até o diretório do Golang inicializar o servidor.
cd ../../go_grpc/ ;
go run server/main.go
E faremos várias chamadas para testar a comunicação da aplicação Dart com o microserviço Golang. Dentro do diretório dart_grpc/bin
, executamos o client_id.dart
.
dart client_id.dart
Na figura 6 vemos no shell superior o microserviço Golang servindo na porta 50051 e na shell inferior a aplicação cliente Dart imprimindo o id gerado pelo servidor Golang.
Figura 6: Aplicação Dart comsumindo microserviço Golang
Agora vamos fazer nosso microserviço de banco de dados Dart cuja chave primária será um inteiro sequencial fornecido pelo microserviço Golang. No mesmo diretório dart_grpc/bin
vamos criar os arquivos server_client_go.dart
e client_id_go.dart
.
Do serviço CrudAlunoService
, a única chamada de procedimento remota que usará o serviço Go é createAluno
.
import 'package:grpc/grpc.dart';
import '../protos/aluno.pbgrpc.dart';
import '../protos/gerador_id.pbgrpc.dart';
import '../protos/google/protobuf/empty.pb.dart' show Empty;
class CrudAlunoService extends CrudAlunoServiceBase {
Alunos lista = Alunos();
late ClientChannel channel;
late GeradorIDClient stub;
@override
Future<Aluno> createAluno(ServiceCall call, Aluno request) async {
print('\n🐦 API Dart - createAluno ________________________________');
var aluno = Aluno();
late int id;
if (request.id == 0) { // buscar id
// Obtendo id do servidor Go
late var message;
channel = ClientChannel('localhost',
port: 50051,
options: // Aqui não teremos credenciais
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = GeradorIDClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
try {
message = await stub.gerarId(Empty());
}
catch (e) {
print('\n\nErro ao obter ID do servidor Go. Talvez o servidor esteja offline\n');
print(e);
}
await channel.shutdown();
id = message.goId;
}
else { // usar o fornecido no request
id = request.id;
}
aluno.nome = request.nome;
aluno.id = id;
lista.alunos.add(aluno);
print('Método createAluno - novo aluno inserido:');
print(aluno.toString());
return aluno;
}
@override
Future<Alunos> getAllAlunos(ServiceCall call, Empty request) async {
print('\n🐦 API Dart - getAllAlunos _______________________________');
if (lista.alunos.isEmpty) {
print('Lista vazia.');
}
for (var aluno in lista.alunos) {
print('${aluno.id} - ${aluno.nome}');
}
print(' ');
return lista;
}
@override
Future<Aluno> getAluno(ServiceCall call, AlunoId request) async {
print('\n🐦 API Dart - getAluno ___________________________________');
print('Resgantando o aluno de id:${request.id}' );
var aluno = lista.alunos.firstWhere((aluno) => aluno.id == request.id);
return aluno;
}
@override
Future<Empty> deleteAluno(ServiceCall call, AlunoId request) async {
print('\n🐦 API Dart - deleteAluno ________________________________');
print('Apagando o aluno de id:${request.id}' );
lista.alunos.removeWhere((aluno) => aluno.id == request.id);
return Empty();
}
@override
Future<Aluno> editAluno(ServiceCall call, Aluno request) async {
print('\n🐦 API Dart - editAluno __________________________________');
print('Editando o aluno de id:${request.id}' );
var aluno = lista.alunos.firstWhere((aluno) => aluno.id == request.id);
aluno.nome = request.nome;
return aluno;
}
}
Future<void> main(List<String> args) async {
final server = Server(
[CrudAlunoService()],
const <Interceptor>[],
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
);
await server.serve(port: 50052);
print('\n🐦 Servidor ouvindo na porta ${server.port}...\n');
}
Listagem 5: server_client_go.dart
Para testar nosso servidor de banco de dados, nós vamos utilizar client_id_go.dart
da listagem 6, que é quase o mesmo código de client.dart
da listagem 4, com a pequena diferença que dessa vez nós não fornecemos o id para o aluno. Nós instanciamos o objeto aluno e atribuímos apenas o nome, o id o servidor irá buscar com o serviço Golang.
import 'package:grpc/grpc.dart';
import './../protos/aluno.pbgrpc.dart';
import '../protos/google/protobuf/empty.pb.dart';
class Client {
late ClientChannel channel;
late CrudAlunoServiceClient stub;
Future<void> main(List<String> args) async {
channel = ClientChannel('localhost',
port: 50052,
options: // Aqui não teremos credenciais
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = CrudAlunoServiceClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
try {
print('\n__ Adicionando Alunos ---------------------------------');
var alunoToAdd1 = Aluno();
alunoToAdd1.nome = "Elias Mantovani Rebouças";
var alunoAdicionado1 = await stub.createAluno(alunoToAdd1);
print("Aluno Adicionado:\n" + alunoAdicionado1.toString());
var alunoToAdd2 = Aluno();
alunoToAdd2.nome = "Pedro Henrique Coimbra";
var alunoAdicionado2 = await stub.createAluno(alunoToAdd2);
print("Aluno Adicionado:\n" + alunoAdicionado2.toString());
print('\n__ Listagem de Alunos ---------------------------------');
var todosAlunos = await stub.getAllAlunos(Empty());
print(todosAlunos.alunos.toString());
print('\n__ Removendo Aluno ------------------------------------');
var alunoToDel = AlunoId();
alunoToDel.id = alunoAdicionado2.id;
await stub.deleteAluno(alunoToDel);
print("Aluno removido com ID: " + alunoToDel.id.toString());
print('\n__ Listagem de Alunos ---------------------------------');
var todosAlunos2 = await stub.getAllAlunos(Empty());
print(todosAlunos2.alunos);
print('\n__ Edição de Aluno ------------------------------------');
var alunoToEdit = Aluno();
alunoToEdit.id = alunoAdicionado1.id;
alunoToEdit.nome = "David Bitcoin";
await stub.editAluno(alunoToEdit);
print("Aluno editado com ID: " + alunoToEdit.id.toString());
print('\n__ Busca do Aluno Editado ---------------------------');
var alunoToGet = AlunoId();
alunoToGet.id = alunoAdicionado1.id;
var alunoObtido = await stub.getAluno(alunoToGet);
print("Aluno de id = 1 com nome editado:\n" + alunoObtido.toString());
}
catch (e) {
print('\n\nErro: O Servidor está offline\n');
print(e);
}
await channel.shutdown();
}
}
main() {
var client = Client();
client.main([]);
}
Listagem 6: client_id_go.dart
Agora vamos precisar de três shell. Um para subir o servidor Golang, um segundo para o servidor Dart e o terceiro para rodar nosso client_id_go.dart
para os testes. Com efeito, temos a figura 7, com a shell superior tendo o servidor Golang ouvindo na porta 50051, e na parte inferior o Dart, com o servidor (server_client_go.dart
) respondendo na port 50052 na shell à esquerda e a aplicação cliente (client_id_go.dart
) à direita.
Figura 7: gRPC integrando APIs Dart e Golang
Por fim, agora nos falta implementar a aplicação cliente que irá consumir o microserviço de sorteio de número NodeJS bem com do serviço de banco de dados Dart. Vamos implementar a aplicação 4. Java.