It is really easy to setup language agnostic open, staggered client-server interactions in a bidirectional, streaming way using the gRPC framework
This is done due to the fact that gRPC runs on HTTP2, so resource multiplexing comes into the picture and helps client and server both keep pushing data and listen and respond accordingly
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/
| |-- max.proto
|-- server/
|-- index.js
|-- protos/
|-- max_pb.js
|-- max_grpc_pb.js
0. Sample Problem
In this exercise, we will be building an API to return the maximum of numbers streamed from client (client streaming) every couple of seconds (server streaming)
The function takes a stream of Request messages that have one integer each, and returns a stream of Responses that represents the maximum of all the nummbers streamed to it upto that point.
This interaction is :
- Bidirectional - as client and server both keep streaming data to each other.
- Staggered - as client and server streams have different speeds
1. Writing the protofile
This is pretty basic and will involve
TYPE | HTML | |
---|---|---|
gRPC Request | STREAM OF INTEGERS |
Simple message object |
gRPC Response | STREAM INTEGER |
Simple message object |
Service | FUNCTION |
rpc CalciMax(stream MaxRequest) returns (stream MaxResponse ){} |
syntax="proto3";
package max;
service MaximumService{
rpc CalciMax(stream MaxRequest) returns (stream MaxResponse ){}
}
message MaxResponse{
int32 result=1;
}
message MaxRequest{
int32 num=1;
}
Here, the stream keyword next to AvgRequest and AvgResponse 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 stream of responses from server.
2. Generate the protofiles
Packages required : protoc, grpc-tools
1. grpc-tools
npm i grpc-tools
We get MaxServiceService and MaxServiceClient 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/max.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
SIDE NOTE
I shall be using the chalk library here to setup beautiful text outputs in our console so we can clearly see how each request and response are communicated
npm i chalk
3. Setup a gRPC server
npm i grpc google-protobuf
Import the necessary dependencies. Here,
- max_pb ⇒ Contains all the details about the request and response object.
- max_grpc_pb ⇒ Will help the grpc server create an API from the proto generated code.
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.
Also, by setting up different sleep intervals for client and server we can establish a staggered communication between them.
4. Setup the Client Side
Import the necessary dependencies.
- max_grpc_pb ⇒ Contains proto generated code that will help us setup the client and link it to the server functions we have defined
- max_pb ⇒ Contains code to help us access and work with the request object
Setup a client using the proto generated code
Add service to server and start it
//* Setup a server
const server = new grpc.Server()
server.addService(maxService.MaximumServiceService, { calciMax })
//? 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 bidirectional streaming type API designing. Thanks a lot for reading this blog. If you like more content like this, subscribe to my mailing list