Client Push API with gRPC

gRPC allows a one-word implementation to write an API where a client sends a continuous stream of data to the server

This is done due to the fact that gRPC runs on HTTP2, so resource multiplexing comes into the picture and helps client keep pushing data while the server listens for it and responds as per requirement.

Resources

I have committed all the protofiles in detailed branches at this github repository for your perusal

crew-guy/gRPC
A repository that explains setting up modern, fast, language agnostic APIs using the gRPC framework - crew-guy/gRPC

This is the file structure that I have followed while designing this API.


root_folder/
|-- client/
|   |-- client.js
|-- node_modules  
|-- protos/
|   |-- avg.proto
|-- server/
     |-- index.js
     |-- protos/
        |-- avg_pb.js
        |-- avg_grpc_pb.js

0. Sample Problem

In this exercise, we will be building a Calculate the Average of streamed numbers using client streaming

The function takes a stream of Request messages that have one integer each, and returns a single Response that represents the average of all the nummbers streamed to it

1. Writing the protofile

This is pretty basic and will involve

TYPE HTML
gRPC Request STREAM OF INTEGERS Simple message object
gRPC Response INTEGER Simple message object
Service FUNCTION rpc ComputeAvg(stream AvgRequest) returns (AvgResponse){}
syntax="proto3";

package avg;

service AvgService{
    rpc ComputeAvg(stream AvgRequest) returns (AvgResponse){}
}

message AvgRequest{
    int32 num = 1;
}

message AvgResponse{
    double average = 1;
}

Here, the stream keyword next to AvgRequest is of key importance as it tells gRPC to generate code accordingly such that the function called from the stream of client request will return single response.

2. Generate the protofiles

Packages required : protoc, grpc-tools

1. grpc-tools

npm i grpc-tools

We get AvgServiceService and AvgServiceClient 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/avg.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,

  1. avg_pb ⇒  Contains all the details about the request and response object.
  2. avg_grpc_pb ⇒  Will help the grpc server create an API from the proto generated code.
const grpc = require("grpc")
const avg = require('./protos/avg_pb')
const avgService = require('./protos/avg_grpc_pb')

Importing the required packages and generated proto files
const computeAvg = (call, callback) =>
{
    const numArray = []
    call.on('error', (error =>
    {
        console.log(error)
    }))

    call.on('end', () =>
    {
        console.log('Average has been computed !')
        const result = new avg.AvgResponse()
        const calc = (numArray.reduce((i, acc)=> i + acc, 0))/numArray.length
        result.setAverage(calc)

        console.log(chalk.blue.bold(`Average : ${result.getAverage()}`))
    })

    call.on('data', request =>
    {
        const num = request.getNum()
        console.log(chalk.blueBright(num))
        numArray.push(num)
    })
}
Function that processes request to return response 

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

Here the call.on() methods allow us to basically monitor the stream sent from the client for proper response and error handling.

4. Setup the Client Side

Import the necessary dependencies.

  1. avg_grpc_pb ⇒ Contains proto generated code that will help us setup the client and link it to the server functions we have defined
  2. avg_pb ⇒ Contains code to help us access and work with the request object
const grpc = require('grpc');
const avg = require('../server/protos/avg_pb')
const avgService = require('../server/protos/avg_grpc_pb')
Importing the necessary packages and generated files

Setup a client using the proto generated code

const URL_ENDPOINT = `localhost:50051` 

const avgClient = new avgService.AvgServiceClient(
    URL_ENDPOINT,
    grpc.credentials.createInsecure()
)
Setup the client from the service hosted at the URL_ENDPOINT
const avgReq = new avg.AvgRequest()
const call = avgClient.computeAvg(avgReq, (error, response) =>
{
    if (!error)
    {
        console.log(`Responded average is ${response}`)
    } else
    {
        console.log(error)
    }
})
Handling the response
const numArray = [1,2,3,4,5,6,7,8,9,10]
let count = 0, intervalID = setInterval(() =>
{
    console.log(chalk.magentaBright(`Sending request num ${count + 1}`))
    avgReq.setNum(numArray[count])
    call.write(avgReq)
    if (++count == numArray.length)
    {
        clearInterval(intervalID)
        call.end()
    }
},1000)
Sending the client requests in a stream

Add service to server and start it

//* Setup a server
const server = new grpc.Server()

server.addService(avgService.AvgServiceService, { computeAvg })

//? 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 client-push-server-stream type API designing. Thanks a lot for reading this blog. If you like more content like this, subscribe to my mailing list