Server Push API with gRPC
gRPC allows a one-word implementation to write an API where a client requests for data once but the server returns a stream of data
This is done due to the fact that gRPC runs on HTTP2, so resource multiplexing comes into the picture and helps server keep pushing data while the client listens for it
Resources
I have committed all the protofiles in detailed branches at this github repository for your perusal
This is the file structure that I have followed while designing this API.
root_folder/
|-- client/
| |-- client.js
|-- node_modules
|-- protos/
| |-- primenum.proto
|-- server/
|-- index.js
|-- protos/
|-- primenum_pb.js
|-- primenum_grpc_pb.js
0. Sample Problem
In this exercise, we will be building a Prime Number Decomposition API using server streaming
The function takes a Request message that has one integer, and returns a stream of Responses that represent the prime number decomposition of that number
Pseudo code
k = 2
N = 210
while N > 1:
if N % k == 0: // if k evenly divides into N
print k // this is a factor
N = N / k // divide N by k so that we have the rest of the number left.
else:
k = k + 1
1. Writing the protofile
This is pretty basic and will involve
TYPE | HTML | |
---|---|---|
gRPC Request | INTEGER |
Simple message object |
gRPC Response | STREAM OF INTEGERS |
Simple message object |
Service | FUNCTION |
rpc CalcPn(PnRequest) returns (stream PnResponse){} |
syntax="proto3";
package primenum;
service PnService {
rpc CalcPn(PnRequest) returns (stream PnResponse){}
}
message PnRequest{
int32 pn = 1;
}
message PnResponse{
int32 result = 1;
}
Here, the stream keyword next to PnResponse is of key importance as it tells gRPC to generate code accordingly such that the function called from the client will return a stream of responses
2. Generate the protofiles
Packages required : protoc, grpc-tools
1. grpc-tools
npm i grpc-tools
We get PnServiceService and PnServiceClient generated from the "grpc-tools" module that will act on our proto to create the client and service (server)
Output file ⇒ PACKAGENAME_grpc_pb.js
2. protoc
Whereas, the protoc will simply generate the Request and Response objects and give us getters and setters on them
Output filename ⇒ PACKAGENAME_pb.js
sudo protoc -I=. ./protos/primenum.proto \
--js_out=import_style=commonjs,binary:./server \
--grpc_out=./server \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin`
Here the inside the main project directory, we have a client, server and protos subdirectory. Currently, we are generating the API from proto file in the protos directory into the protos subdirectory inside the server directory
3. Setup a gRPC server
npm i grpc google-protobuf
Import the necessary dependencies. Here,
- primenum_pb ⇒ Contains all the details about the request and response object
- primenum_grpc_pb ⇒ Will help the grpc server create an API from the proto generated code.
const grpc = require("grpc")
const pn = require('./protos/primenum_pb')
const pnService = require('./protos/primenum_grpc_pb')
const grpc = require("grpc")
const chalk = require('chalk')
// Define the function (camelcased version of the function described in the protofile) that will do the calculation
const calcPn = (call, callback) =>
{
let pnReq = call.request.getPn()
console.log(chalk.greenBright(pnReq))
let pnTemp = pnReq
for(let i = 2; i < pnReq; i++)
{
while (pnTemp % i === 0)
{
pnTemp = pnTemp / i
const pnRes = new pn.PnResponse()
pnRes.setResult(i)
call.write(pnRes)
}
}
call.end()
}
// Add this service request to the server
server.addService(pnService.PnServiceService, { calcPn })
// START THE SERVER
const URL_ENDPOINT = "127.0.0.1:50051"
server.bind(URL_ENDPOINT, grpc.ServerCredentials.createInsecure())
server.start()
console.log(chalk.yellow(`Server running on ${URL_ENDPOINT}`))
Define the function that will handle the request and return a response. This function is what we defined in the proto file and this is also the function that will be callled from the client
4. Setup the Client Side
Import the necessary dependencies.
- primenum_grpc_pb ⇒ Contains proto generated code that will help us setup the client and link it to the server functions we have defined
- primenum_pb ⇒ Contains code to help us access and work with the request object
const grpc = require('grpc');
const pn = require('../server/protos/primenum_pb')
const pnService = require('../server/protos/primenum_grpc_pb')
Setup a client using the proto generated code
const pnServiceClient = new pnService.PnServiceClient(
URL_ENDPOINT,
grpc.credentials.createInsecure()
)
Configure and send the request object
const pnServiceClient = new pnService.PnServiceClient(
URL_ENDPOINT,
grpc.credentials.createInsecure()
)
const pnServiceReq = new pn.PnRequest()
pnServiceReq.setPn(20)
const call = pnServiceClient.calcPn(pnServiceReq, () => {})
Handle the response received on the client side
call.on('status', (status) =>
{
console.log(status)
})
call.on('data', (response) =>
{
console.log(`Factor : ${response}`)
})
call.on('error', (error) =>
{
console.log(error)
})
call.on('end', () =>
{
console.log('Streaming has ended ........')
})
Add service to server and start it
//* Setup a server
const server = new grpc.Server()
server.addService(pnService.PnServiceService, { calcPn })
//? START THE SERVER
const URL_ENDPOINT = "127.0.0.1:50051"
server.bind(URL_ENDPOINT, grpc.ServerCredentials.createInsecure())
server.start()
console.log(chalk.green(`Server running on ${URL_ENDPOINT}`))
Conclusion
Congratulations ! You are now equipped with the futuristic skills of server-push-client-stream type API designing. Thanks a lot for reading this blog. If you like more content like this, subscribe to my mailing list