Uma introdução ao gRPC | Node Hello World
O gRPC (acrônimo recursivo para "gRPC Remote Procedure Call") é um framework lançado pelo Google em 2015, que utilizada HTTP2 em sua camada de transporte e Protobuf (Protocol Buffer) tanto como sua IDL (Interface Description Language,) definindo seus contratos, serviços e mensagens, como o protocolo de serialização para o transporte de dados (uma serialização binária neste caso).
Características do gRPC
O gRPC funciona em modo client/server, sendo que tanto o cliente quanto o servidor podem realizar o envio de mensagens dos tipos unário (uma mensagem) ou stream de mensagens.
Uma característica essencial do gRPC é a interoperabilidade, pois nele usamos uma linguagem padrão neutra para declarar seu contrato (o protobuf) e utilizamos um compilador para gerar os stubs utilizados em diferentes linguagens. Isto permite combinar diferentes tecnologias em uma arquitetura de microsserviços por exemplo, contando com um contrato forte e com um protocolo de serialização binário e uma comunicação eficiente.
HTTP2
Uma das peças chave para atingir os excelentes resultados do gRPC em desempenho são as características modernas do HTTP2 como a multiplexação de requests aproveitando a mesma conexão, o fato de ser um protocolo binário e não texto (como o http) e o server push.
Protobuf
O Protobuf é uma linguagem declarativa também desenvolvida pelo Google. Ele funciona como uma lingua franca onde você declara os serviços e mensagens (Requests/Responses) em arquivos com a extensão .proto. Posteriormente se utiliza um compilador proto para gerar o código correspondente em diversas linguagens.
E é claro que este processo pode ser controlado e automatizado com ferramentas como maven e outras. Uma informação importante é que a versão estável corrente do protobuf é a v2, mas é recomendado que para gRPC você utilize a v3, para isto basta adicionar na primeira linha do seu arquivo .proto:
syntax = "proto3";
É possível declarar um package opcional no arquivo .proto para evitar conflitos de nome entre mensagem:
package xpto.exemplo;
Uma possibilidade é a utilização de pacotes para versionamento dos seus serviços/mensagens:
package xpto.exemplo.v1;
package xpto.exemplo.v2;
As mensagens que serão trafegadas você declara:
message HelloRequest {
string name = 1;
int32 times = 2;
}
message HelloResponse {
string msg = 1;
}
Além de string e int32 existem diversos outros tipos (Protobuf types).
Cada campo na definição da mensagem possui um número exclusivo ( = 1, = 2 ...). Esses números são usados para identificar os campos na forma binária da mensagem e não devem ser alterados depois que sua mensagem estiver em uso por algum serviço (Mais detalhes).
E um serviço você declara assim:
// Obs: Este é um comentário
/* Este bloco também é um comentário ...
* Esta é a declaração do serviço "Greeter" que fornece 4 metodos (SayHello, SayHelloNTimes, SayHelloToEveryOne, SayHelloToEachOne)
*/
service Greeter {
// recebe uma mensagem HelloRequest como entrada (client unary) e retorna uma mensagem HelloResponse (server unary)
rpc SayHello (HelloRequest) returns (HelloResponse) {}
// recebe uma mensagem HelloRequest como entrada (client unary) e retorna um stream de mensagens HelloResponse (server stream)
rpc SayHelloNTimes (HelloRequest) returns (stream HelloResponse) {}
// recebe um stream de mensagens HelloRequest como entrada (client stream) e retorna uma mensagem HelloResponse (server unary)
rpc SayHelloToEveryOne (stream HelloRequest) returns (HelloResponse) {}
// recebe um stream de mensagens HelloRequest como entrada (client stream) e retorna um stream de mensagens HelloResponse (server stream)
rpc SayHelloToEachOne (stream HelloRequest) returns (stream HelloResponse) {}
// Obs: os streams são assíncronos
}
Compilando as stubs para node.js
Para compilar para node.js será necessário instalar o plugin para js, que será posteriormente passado como parâmetro para o protoc (compiler protobuf) para ser usado para gerar o output.
npm i grpc-tools --save-dev
Compilando com protoc:
protoc --js_out=import_style=commonjs,binary:./ --grpc_out=. --plugin=protoc-gen-grpc=node_modules/grpc-tools/bin/grpc_node_plugin lib/proto/hello.proto
Com isto serão gerados os arquivos:
lib/proto/hello_pb.js
Contendo o contrato da(s) mensagem(s)
lib/proto/hello_grpc_pb.js
Contendo a interface do(s) serviço(s)
Implementando nosso "Hello world!" em node.js
Implementando o server:
const messages = require("./lib/proto/hello_pb")
const services = require("./lib/proto/hello_grpc_pb")
const grpc = require("grpc")
const PORT = 8000
const buildResponse = (name) => {
const resp = new messages.HelloResponse()
resp.setMsg( "Hello " + name + "!" )
return resp
}
/* Funções que serão mapeadas para os métodos correspondentes
* no serviço Greeter
*/
// unary / unary
const sayHello = (call, callback) => {
console.log("# sayHello")
const resp = buildResponse( call.request.getName() )
callback(null, resp)
}
// unary / stream
const sayHelloNTimes = (call) => {
console.log("# sayHelloNTimes")
const times = call.request.getTimes()
for(let i=0; i<times; i++) {
const resp = buildResponse( call.request.getName() )
call.write(resp)
}
call.end()
}
// stream / unary
const sayHelloToEveryOne = (call, callback) => {
console.log("# sayHelloToEveryOne")
const names = []
call.on('data', (data) => {
names.push( data.getName() )
})
call.on('end', () => {
callback(null, buildResponse( names.join(', ') ))
})
}
// stream / stream
const sayHelloToEachOne = (call, callback) => {
console.log("# sayHelloToEachOne")
call.on('data', (data) => {
call.write( buildResponse( data.getName() ) )
})
call.on('end', () => call.end())
}
// A função main() inicia os serviços
const main = () => {
const server = new grpc.Server()
/* Fazendo os binds das funções declaradas para cada método do serviço Greeter
* e adicionando o serviço ao servidor grpc.
*/
server.addService(services.GreeterService, {
sayHello: sayHello,
sayHelloNTimes: sayHelloNTimes,
sayHelloToEveryOne: sayHelloToEveryOne,
sayHelloToEachOne: sayHelloToEachOne
})
server.bind('0.0.0.0:'+PORT, grpc.ServerCredentials.createInsecure())
console.log('starting grpc server on port', PORT)
server.start()
}
// Invocando a função main() e iniciando o servidor grpc
main()
Implementando o client:
const messages = require("./lib/proto/hello_pb")
const services = require("./lib/proto/hello_grpc_pb")
const grpc = require("grpc")
const PORT = 8000
const unaryUnary = (remote) => {
const req = new messages.HelloRequest()
req.setName("Jonas")
remote.sayHello(req, function(err, response) {
console.log('# unaryUnary # Remote said: ', response.getMsg())
})
}
const unaryStream = (remote) => {
const req = new messages.HelloRequest()
req.setName("Maria")
req.setTimes(5)
let call = remote.sayHelloNTimes(req)
call.on('data',function(response){
console.log('# unaryStream # Remote said: ', response.getMsg())
})
call.on('end',function(){
console.log('# unaryStream # End of stream');
})
}
const streamUnary = (remote) => {
const names = ['Lucas', 'Marcela', 'Alice', 'Artur', 'João']
const call = remote.sayHelloToEveryOne(function(err, response) {
console.log('# streamUnary # Remote said: ', response.getMsg())
})
names.forEach( (name) => {
const req = new messages.HelloRequest()
req.setName(name)
call.write(req)
})
call.end()
}
const streamStream = (remote) => {
const names = ['Lucas', 'Marcela', 'Alice', 'Artur', 'João']
const call = remote.sayHelloToEachOne()
call.on('data',function(response){
console.log('# streamStream # Remote said: ', response.getMsg())
})
names.forEach( (name) => {
const req = new messages.HelloRequest()
req.setName(name)
call.write(req)
})
call.end()
}
const main = () => {
const remote = new services.GreeterClient('localhost:'+PORT,
grpc.credentials.createInsecure() )
unaryUnary(remote)
setTimeout( () => unaryStream(remote), 100)
setTimeout( () => streamUnary(remote), 200)
setTimeout( () => streamStream(remote), 300)
}
main()
Execução do server:
$> node server.js
starting grpc server on port 8000
# sayHello
# sayHelloNTimes
# sayHelloToEveryOne
# sayHelloToEachOne
Execução do cliente:
$> node client.js
# unaryUnary # Remote said: Hello Jonas
# unaryStream # Remote said: Hello Maria
# unaryStream # Remote said: Hello Maria
# unaryStream # Remote said: Hello Maria
# unaryStream # Remote said: Hello Maria
# unaryStream # Remote said: Hello Maria
# unaryStream # End of stream
# streamUnary # Remote said: Hello Lucas, Marcela, Alice, Artur, João
# streamStream # Remote said: Hello Lucas
# streamStream # Remote said: Hello Marcela
# streamStream # Remote said: Hello Alice
# streamStream # Remote said: Hello Artur
# streamStream # Remote said: Hello João
$>
Esta implementação em node pode ser encontrada neste
repo git.
Futuramente trarei implementações deste hello em outras linguagens.
Por hoje é isto ...
Artus