gRPC (recursively stands for "gRPC Remote Procedure Call") is a framework released by Google in 2015 that uses HTTP2 for its transport layer and Protobuf (Protocol Buffer) both as its IDL (Interface Description Language) for defining its contracts, services, and messages, as well as the serialization protocol for data transport (a binary serialization in this case).
Features of gRPC
gRPC works in a client/server mode, where both the client and server can send unary messages (a single message) or streams of messages.
A key feature of gRPC is interoperability, as it uses a neutral standard language (Protobuf) to declare its contract and utilizes a compiler to generate stubs used in different languages. This allows combining various technologies in a microservices architecture with strong contracts, a binary serialization protocol, and efficient communication.
HTTP2
One of the key components to achieve gRPC's excellent performance is the modern features of HTTP2, such as request multiplexing over the same connection, its binary protocol (unlike text-based HTTP), and server push.
Protobuf
Protobuf is a declarative language also developed by Google. It acts as a lingua franca where you declare services and messages (Requests/Responses) in .proto
files. You then use a proto compiler to generate the corresponding code in various languages.
Of course, this process can be controlled and automated using tools like Maven(On Java/JVM environments) and others tools.
It is worth noting that the stable and recommended version of protobuf is v3, to use it just add the following to the first line of your .proto file:
syntax = "proto3";
You can optionally declare a package in the .proto file to avoid naming conflicts between messages:
package xpto.example;
You can also use packages for versioning your services/messages:
package xpto.example.v1;
package xpto.example.v2;
Declare the messages to be transmitted as follows:
message HelloRequest {
string name = 1;
int32 times = 2;
}
message HelloResponse {
string msg = 1;
}
Besides string and int32, there are various other types (Protobuf types).
Each field in the message definition has a unique number (e.g., = 1, = 2). These numbers are used to identify fields in the binary form of the message and should not be changed after your message is in use by a service (More details).
Declare a service as follows:
// Note: This is a comment
/* This block is also a comment...
* This declares the "Greeter" service, which provides four methods (SayHello, SayHelloNTimes, SayHelloToEveryOne, SayHelloToEachOne)
*/
service Greeter {
// Takes a HelloRequest message as input (client unary) and returns a HelloResponse message (server unary)
rpc SayHello (HelloRequest) returns (HelloResponse) {}
// Takes a HelloRequest message as input (client unary) and returns a stream of
// HelloResponse messages (server stream)
rpc SayHelloNTimes (HelloRequest) returns (stream HelloResponse) {}
// Takes a stream of HelloRequest messages as input (client stream) and returns a
// HelloResponse message (server unary)
rpc SayHelloToEveryOne (stream HelloRequest) returns (HelloResponse) {}
// Takes a stream of HelloRequest messages as input (client stream) and returns
// a stream of HelloResponse messages (server stream)
rpc SayHelloToEachOne (stream HelloRequest) returns (stream HelloResponse) {}
// Note: Streams are asynchronous
}
Compiling stubs for node.js
To compile for node.js you will need to install the js plugin, which will later be passed as a parameter to protoc (protobuf compiler) to be used to generate the 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
This will generate the following files:
lib/proto/hello_pb.js
Containing the message contract(s)
lib/proto/hello_grpc_pb.js
Containing the service interface(s)
Implementing our "Hello world!" in node.js
Implementing the 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
}
/* Functions that will be mapped to the corresponding methods
* in the Greeter service
*/
// 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())
}
// The main() function starts the services
const main = () => {
const server = new grpc.Server()
/* Binding the declared functions to each method of the Greeter service
* and adding the service to the grpc server.
*/
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()
}
// Invoking the main() function and starting the grpc server
main()
Implementing the 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()
Server execution:
$> node server.js
starting grpc server on port 8000
# sayHello
# sayHelloNTimes
# sayHelloToEveryOne
# sayHelloToEachOne
Client Execution:
$> 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
$>
This implementation in node can be found in this git repo.
In the future I will bring implementations of this hello in other languages.
That's all folks...
Artus