Skip to content

Artus Rocha

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