Elevating Inter-Service Communication
A Comprehensive Exploration of gRPC's High-Performance, Language-Agnostic Approach to Microservices Connectivity
In today's tech-driven world, building microservices is a popular choice for maximizing advantages. Yet, communication hurdles, especially concerning delays, pose a challenge. Selecting the right architectural style is a crucial decision. In this blog, we'll explore one of the most effective patterns used in microservices communication – Remote Procedure Call (RPC).
RPC (Remote Procedure call)
RPC is a form of Inter-process communication (If you're wondering about Inter-process communication, take a look at this blog), is a powerful technique for constructing distributed, client-server-based applications. It is based on extending the conventional local procedure calling so that the called procedure need not exist in the same address space as the calling procedure.
ha, what does it mean?
RPC is like asking another computer (which might be in a different location or address space ) to perform a specific task or function for you. You're essentially reaching out to that distant computer, telling it to execute a function, and then waiting for the results to be sent back to you. It's a way for computers to work together, even if they are not physically next to each other, by requesting and providing services remotely.
EvenMore simply, it makes a network call look more like a local function call
In RPC, several key terminologies play vital roles in orchestrating communication between clients and servers.
Client: This is the system initiating a request, and it can be implemented within any architectural style. It is responsible for making requests to the RPC server.
Server: The RPC server receives client requests, processes them by executing the corresponding functions, and responds to the client. The server may employ different architectural patterns, but its fundamental role in handling RPC requests remains consistent.
API Contract: At the core of RPC lies the API contract, which acts as the interface definition. This contract specifies the function names, request structures, and response formats agreed upon by both the client and server. It serves as the blueprint for their communication.
Stub: The Stub is the linchpin in the RPC lifecycle. Serving as the RPC hero, it takes care of the intricacies of serialization, initiates the request call, and handles deserialization upon receiving responses. The Stub abstracts away implementation details, making the RPC process resemble a local function call. This crucial component streamlines the communication between client and server, encapsulating the complexities and enhancing the simplicity of remote procedure calls. Unlike REST APIs, RPC eliminates the need for manual serialization and deserialization, thanks to the adept handling by the Stub.
It's time to make our hands dirty.
Let's delve into the implementation of RPC using gRPC, a widely adopted RPC framework developed by Google.
gRPC utilizes a proto file as an Interface Definition Language (IDL) to articulate functions, requests, and responses. The significance of the Interface Definition lies in its role as a contract, explicitly specifying the functions along with the corresponding request and response parameters acceptable between the client and server. Therefore, the initial step in establishing a gRPC server is the creation of this Interface Definition.
Let us create an proto file which has an Add function that accepts two numbs as praameter and returns the sum of it .
Breaking down the proto
syntax = "proto3": Indicates that this file is using proto3 syntax, which is the third major version of Protocol Buffers.
package adder: Defines a package named "adder." Packages help organize and namespace the definitions within the Protocol Buffers file.
service AdderService { rpc Add(AddRequest) returns (AddResponse); }:
service AdderService: Declares a gRPC service named "AdderService."
rpc Add(AddRequest) returns (AddResponse): Defines an RPC (Remote Procedure Call) method named "Add" within the service. It takes an AddRequest as a parameter and returns an AddResponse.
message AddRequest { int32 num1 = 1; int32 num2 = 2; }:
message AddRequest: Declares a message type named "AddRequest."
int32 num1 = 1; int32 num2 = 2: Specifies two fields within the message, both of type int32. Field num1 has a tag of 1, and num2 has a tag of 2. Tags are used to identify fields uniquely.
message AddResponse { int32 result = 1; }:
message AddResponse: Declares a message type named "AddResponse."
int32 result = 1: Specifies a single field within the message, named result, of type int32 with a tag of 1.
On succesfull creation of the Interface definition its time to create our gRPC server
Importing Required Modules:
@grpc/grpc-js is the official gRPC library for JavaScript. It provides the core functionality for working with gRPC in Node.js environments
@grpc/proto-loader is responsible for loading and parsing Protocol Buffers (proto) files and generating the necessary JavaScript code, including the client-side stub.
path.join(__dirname,'adder.proto') gets the path to the interface definition file
const packageDefinition = protoLoader.loadSync(PROTO_PATH) This line loads and parses the contents of the adder.proto file synchronously.
const adder_proto = grpc.loadPackageDefinition(packageDefinition).adder The adder_proto object now contains the generated JavaScript code, including the server-side stub based on the definitions in the adder.proto file.
It includes the server-side stub, which receives the requests, processes them, and sends back responses.
server.addService this function holds the implementation of the functions that are declared in the proto file.
server.bindAsync This section binds the server to a specified port (localhost:50051) and starts the server. The server uses insecure credentials (createInsecure()) for simplicity in this example. In a production environment, secure credentials should be used. The console logs indicate whether the server started successfully or encountered an error.
this script creates a gRPC server with a single method (Add) and starts it on the specified port. The server implementation simply adds two numbers received in the request and sends back the result.
Now our server is ready its time to create our client and try to access the gRPC server.
A Node.js server acts as a gRPC client, making an RPC call to a gRPC server when the specified endpoint is accessed.
these lines are extracting values from the query parameters of an HTTP request made to the endpoint.
the process of loading the proto file is the same as what we did in the server the only difference here is the adder_proto contains the client-side stub which is responsible for making remote procedure calls to the server. It abstracts away the details of network communication.
The result
Based on the provided image, it is apparent that when a request is made to the Node server, the server identifies the registered endpoint. The implementation of this endpoint involves initiating an RPC call to the gRPC server, which processes the request and sends a response back to the Node server. The Node server, in turn, responds to the original client, completing the communication cycle.
You may ask what is the benefit of gRPC over the other architecture styles?
Efficiency: gRPC uses a binary serialization format (Protocol Buffers) and supports HTTP/2, leading to more compact payloads and reduced latency compared to traditional text-based formats used in REST.
Code Generation: gRPC allows the generation of client and server code from a single source of truth, the Protocol Buffers file. This reduces manual effort, ensures consistency, and provides strong typing. ( as you can see that in the above example we have created the server and client logic using the same proto file ).
Bidirectional Streaming: gRPC supports bidirectional streaming, allowing both clients and servers to send a stream of messages. This is beneficial for use cases such as real-time updates and interactive communication.
Service Contract: The Interface Definition Language (IDL) used in gRPC defines a clear contract between the client and server, ensuring a shared understanding of the API. This promotes better collaboration and compatibility.
Language Agnostic: gRPC supports multiple programming languages, enabling polyglot development. Clients and servers can be implemented in different languages while still communicating seamlessly.
This means the implementation can be made in any of the languages such as java, python, golang etc., using the proto file.
For example, Assume the client is implemented on javascript and RPC server is implemented on java
the client-stub will take care of converting the proto file to js code and then it is serialized into binary format then in the RPC server, the server-stub is responsible for converting this code to java class/object and executes the function and response back.
github : https://github.com/uvaprasaath/grpc-example-.git
Conclusion
As we conclude, gRPC stands as a compelling solution for modern microservices communication, offering a blend of efficiency, language flexibility, and high-performance networking. It seamlessly combines efficiency, language adaptability, and robust networking performance. However, it is essential to judiciously assess whether its adoption aligns with specific project requirements. While gRPC offers notable advantages, its application is most beneficial when tailored to meet the precise needs and complexities of a microservices ecosystem. Therefore, the decision to incorporate gRPC should be driven by a careful consideration of the project's unique demands, ensuring that its advantages align with the desired outcomes.