What is gRPC? Fundamental Concepts Explained With Hands-On Example

What is gRPC? Fundamental Concepts Explained With Hands-On Example

Overview

In simple terms, gRPC is a modern open-source framework for Remote Procedure Calls (RPC) that can in run in any environment.

Now, let's break down what RPC means:

RPC, or Remote Procedure Call, is an interprocess communication technique that is used for point-to-point communications between different processes or programs that may be running on separate computers or networked systems. In order words, it enables a program to execute code in a different address space or on a different machine as if it were a regular function call within the same program. Instead of just talking within the same system like in Local Procedure Calls or LPC, RPC turns regular function calls into messages that can travel between different computers or networked systems. This is a fundamental concept behind how gRPC operates, and it's widely useful in microservice systems.

While gRPC is popular, other RPC frameworks like Thrift, JSON-RPC, and XML-RPC also exist.

Now, let's explore another crucial aspect:

Understanding Protobuf or Protocol Buffers

Protocol Buffers, often referred to as Protobuf, are a way to package and send data efficiently across networks or store it. It's the default format for data in gRPC, and it significantly boosts data exchange for better performance. Think of it as a faster and more efficient version of JSON, but with a special twist—it uses .proto files for data definitions.

Protobuf provides strong typing through schema definitions in these proto files, simplifying the creation of data access classes for various programming languages supported by gRPC. The underlying tool responsible for the generation of data access classes for the various programming languages is called Protoc, which stands for Protocol Buffers Compiler. It takes .proto files as input and generates code in different languages like Python, Java, C++, Go, and more. This flexibility makes protoc a powerful tool for enabling cross-language communication in projects like gRPC.

To illustrate further, consider a User management system defined in Protobuf format below. It's not bound to any specific programming language and can work in any environment. You can generate code for the server or client in any preferred programming language:

// users.proto file
syntax = "proto3"; // Specifies the version of Protocol Buffers used (proto3 in this case).

message User { // Defines a message type called "User."
    string id = 1; // User message has a field named "id" of type string with tag 1.
    string name = 2; // Another field "name" of type string with tag 2.
    int32 age = 3; // Field "age" of type int32 with tag 3.
    string address = 4; // Field "address" of type string with tag 4.
}

message UserList { // Defines another message type called "UserList."
    repeated User users = 1; // "UserList" has a repeated field "users" containing multiple User messages.
}

service UserService { // Defines an RPC service called "UserService."
    rpc GetAll (Empty) returns (UserList) {} // RPC method "GetAll" takes no input (Empty) and returns a UserList.
    rpc Get (UserId) returns (User) {} // RPC method "Get" takes a UserId and returns a User.
    rpc Insert (User) returns (User) {} // RPC method "Insert" takes a User and returns a User.
    rpc Update (User) returns (User) {} // RPC method "Update" takes a User and returns a User.
    rpc Remove (UserId) returns (Empty) {} // RPC method "Remove" takes a UserId and returns an Empty message.
}

message Empty {} // Defines an empty message type called "Empty."

message UserId { // Defines a message type called "UserId."
    string id = 1; // "UserId" message has a single field "id" of type string with tag 1.
}

Architecture and Fundamental Concepts.

Many key gRPC features rely on the capabilities unlocked by its utilization of the HTTP/2 protocol. At its essence, HTTP/2 enables simultaneous multiple requests to the server. Here are key insights into gRPC's architecture and fundamental concepts to help you better understand it:

Request/Response Multiplexing:

In gRPC, the underlying HTTP/2 protocol enables a feature called Request/Response Multiplexing. This means that multiple requests and responses can be sent and received concurrently over a single network connection. It's like having multiple conversations happening at the same time within a single connection, making data exchange more efficient.

Header Compression:

gRPC utilizes a technique called HPack for header compression. When sending data, including header information, HPack encodes and compresses it. This results in reduced overhead and improved performance because smaller packets of data are transmitted, saving bandwidth.

Metadata:

Instead of traditional HTTP request headers, gRPC uses metadata, which is essentially key-value data pairs. Both the client and the server can set metadata to convey additional information along with the request or response. It's a flexible way to include extra context in the communication. Additionally, the server can send optional trailing metadata after the main response data.

Streaming:

gRPC supports various streaming types, which is another core concept. There are four types: unary, bi-directional, client streaming, and server streaming.

  • Unary streaming involves a single request and a single response just like a normal function call.

  • Client streaming allows a stream of messages from the client to the server instead of a single message. The server responds with a single message.

  • Server streaming is when the server returns a stream of messages in response to a client’s request.

  • Lastly, bi-directional streaming enables the client and server to send a stream of data in any order concurrently creating a dynamic and interactive communication channel.

Interceptors:

Interceptors in gRPC function similarly to middleware in HTTP REST APIs. They allow you to intercept and handle RPC calls at various points in the request-response cycle. This is useful for implementing cross-cutting concerns such as authentication, logging, and error handling in a modular and reusable way.

Load Balancing:

Load balancing is a mechanism that ensures client requests are evenly distributed across multiple servers. This improves the reliability and scalability of the system by preventing any single server from being overwhelmed with requests. It ensures that each server in a cluster shares the load effectively.

Call Cancellation:

This feature is particularly valuable in scenarios involving server-side streaming, where multiple server requests might be in progress. Clients can cancel a streaming operation if they no longer need the data. This helps manage network resources efficiently.

Deadlines/Timeouts:

gRPC enables clients to set deadlines or timeouts for RPC calls. Clients can specify how long they are willing to wait for an RPC to complete before it's terminated with an DEADLINE_EXCEEDED error. This ensures that RPC calls do not hang indefinitely, improving system responsiveness and resource management.

These core concepts collectively make gRPC a powerful and efficient communication framework, suitable for building modern, high-performance distributed systems

gRPC in Microservices Communication

Among its various strengths, gRPC truly shines in the realm of microservices architecture. It has emerged as one of the go-to inter-service communication mechanisms within data centres. In a microservices-based setup, where services need to communicate seamlessly, gRPC streamlines this process. It offers a reliable and efficient way for microservices to exchange data, ensuring that the distributed system functions smoothly. So, whether one is orchestrating a complex network of microservices or building a robust cloud-native application, gRPC simplifies and accelerates such inter-service communication.

Practical Example Using Node.js

In this section, we'll demonstrate how to use gRPC in a Node.js environment to explore its capabilities. If you're interested in implementing gRPC in other programming languages, you can find the documentation here.

To get started, we will build a user management system that has a basic CRUD (Create, Read, Update, Delete) functionality using Node.js, Express, Handlebars, and gRPC. Throughout the code, you'll find comments that provide explanations for each code block's purpose. Below is the file structure we'll be using:

project-root/
│
├── client/
│   ├── views/
│   │   ├── layout/
│   │   │   └── main.handlebars
│   │   └── user.handlebars
│   ├── client.js
│   └── index.js
│
├── server/
│   └── server.js
│
└── protos/
    └── users.proto

Step 1: Setting up the Node.js project

Create a New Directory: Create a new directory for your project and navigate to it using your terminal.

mkdir grpc-practice
cd grpc-practice

Initialize a Node.js Project: Initialize a new Node.js project by running the following command and following the prompts:

yarn init -y

Install Dependencies: Install the required dependencies using yarn or npm

yarn add @grpc/grpc-js @grpc/proto-loader body-parser express express-handlebars uuid

Additionally, add the scripts for launching both the server and the client. Consequently, the package.json file will look like this:

{
  "name": "grpc-practice",
  "version": "1.0.0",
  "license": "MIT",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server/server.js",
    "startClient": "node client/index.js"
  },
  "dependencies": {
    "@grpc/grpc-js": "^1.9.5",
    "@grpc/proto-loader": "^0.7.10",
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "express-handlebars": "^7.1.2",
    "uuid": "^9.0.1"
  }
}

Step 2: Define the User Message in Protobuf

// users.proto file
syntax = "proto3"; // Specifies the version of Protocol Buffers used (proto3 in this case).

message User { // Defines a message type called "User."
    string id = 1; // User message has a field named "id" of type string with tag 1.
    string name = 2; // Another field "name" of type string with tag 2.
    int32 age = 3; // Field "age" of type int32 with tag 3.
    string address = 4; // Field "address" of type string with tag 4.
}

message UserList { // Defines another message type called "UserList."
    repeated User users = 1; // "UserList" has a repeated field "users" containing multiple User messages.
}

service UserService { // Defines an RPC service called "UserService."
    rpc GetAll (Empty) returns (UserList) {} // RPC method "GetAll" takes no input (Empty) and returns a UserList.
    rpc Get (UserId) returns (User) {} // RPC method "Get" takes a UserId and returns a User.
    rpc Insert (User) returns (User) {} // RPC method "Insert" takes a User and returns a User.
    rpc Update (User) returns (User) {} // RPC method "Update" takes a User and returns a User.
    rpc Remove (UserId) returns (Empty) {} // RPC method "Remove" takes a UserId and returns an Empty message.
}

message Empty {} // Defines an empty message type called "Empty."

message UserId { // Defines a message type called "UserId."
    string id = 1; // "UserId" message has a single field "id" of type string with tag 1.
}

Step 2: Configure the Server

// server.js

// Import necessary modules and libraries
const PROTO_PATH = "./protos/users.proto"; // Path to the Protobuf definition file
import {
  loadPackageDefinition,
  Server,
  status,
  ServerCredentials,
} from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";
import { v4 as uuidv4 } from "uuid";

// Initial data for user records
const users = [
  {
    id: "a68b823c-7ca6-44bc-b721-fb4d5312cafc",
    name: "John Bolton",
    age: 23,
    address: "Address 1",
  },
  {
    id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7",
    name: "Mary Anne",
    age: 45,
    address: "Address 2",
  },
];

// Load and configure Protobuf definitions
var packageDefinition = loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  arrays: true,
});

var usersProto = loadPackageDefinition(packageDefinition);

// Configure the gRPC server
const server = new Server();

server.addService(usersProto.UserService.service, {
  // Define gRPC service methods

  // Get all users
  getAll: (_, callback) => {
    callback(null, { users: users });
  },

  // Get a user by ID
  get: (call, callback) => {
    let user = users.find((n) => n.id == call.request.id);

    if (user) {
      callback(null, user);
    } else {
      callback({
        code: status.NOT_FOUND,
        details: "User not found",
      });
    }
  },

  // Insert a new user
  insert: (call, callback) => {
    let user = call.request;

    // Generate a unique ID for the new user
    user.id = uuidv4();
    users.push(user);
    callback(null, user);
  },

  // Update an existing user
  update: (call, callback) => {
    let existingUser = users.find((n) => n.id == call.request.id);

    if (existingUser) {
      existingUser.name = call.request.name;
      existingUser.age = call.request.age;
      existingUser.address = call.request.address;
      callback(null, existingUser);
    } else {
      callback({
        code: status.NOT_FOUND,
        details: "User not found",
      });
    }
  },

  // Remove a user by ID
  remove: (call, callback) => {
    let existingUserIndex = users.findIndex(
      (n) => n.id == call.request.id
    );

    if (existingUserIndex != -1) {
      users.splice(existingUserIndex, 1);
      callback(null, {});
    } else {
      callback({
        code: status.NOT_FOUND,
        details: "User not found",
      });
    }
  },
});

// Bind the server to a specified IP address and port
server.bindAsync("127.0.0.1:30043", ServerCredentials.createInsecure(), (err, port) => {
  if (!err) {
    console.log(`Server running at http://127.0.0.1:${port}`);
    server.start();
  } else {
    console.error("Error binding to the port:", err);
  }
});

Step 3: Configure the Client

// client.js

// Import necessary modules and libraries
const PROTO_PATH = "./protos/users.proto"; // Path to the Protobuf definition file
import { loadPackageDefinition, credentials } from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";

const packageDefinition = loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  arrays: true,
});

const UserService = loadPackageDefinition(packageDefinition).UserService;

const client = new UserService(
  "localhost:30043",
  credentials.createInsecure()
);

export default client;

Step 4: Define Client-Side Routes

// client.js
// Import necessary modules and libraries
import client from "./client.js"; // Import the gRPC client
import { join } from "path";
import express from "express";
import bodyParser from "body-parser";
import { engine } from "express-handlebars";

import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();

app.set("views", join(__dirname, "views"));
app.engine("handlebars", engine());
app.set("view engine", "handlebars");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Route to display the list of users
app.get("/", (req, res) => {
  // Use gRPC client to fetch all users and render the "users" view
  client.getAll(null, (err, data) => {
    if (!err) {
      res.render("users", { results: data.users });
    }
  });
});

// Route to handle the creation of a new user
app.post("/save", (req, res) => {
  // Create a new user object from the request data
  let newUser = {
    name: req.body.name,
    age: req.body.age,
    address: req.body.address,
  };

  // Use gRPC client to insert the new user
  client.insert(newUser, (err, data) => {
    if (err) throw err;

    console.log("User created successfully", data);
    res.redirect("/");
  });
});

// Route to handle the updating of an existing user
app.post("/update", (req, res) => {
  // Create an updated user object from the request data
  const updateUser = {
    id: req.body.id,
    name: req.body.name,
    age: req.body.age,
    address: req.body.address,
  };

  // Use gRPC client to update the user
  client.update(updateUser, (err, data) => {
    if (err) throw err;

    console.log("User updated successfully", data);
    res.redirect("/");
  });
});

// Route to handle the removal of a user
app.post("/remove", (req, res) => {
  // Use gRPC client to remove the user by ID
  client.remove({ id: req.body.user_id }, (err, _) => {
    if (err) throw err;

    console.log("User removed successfully");
    res.redirect("/");
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log("Server running at port %d", PORT);
});

Step 5: Create the User Interface

Here are the steps to set up the user interface:

  • Add the following code to the main.handlebars file:
<html lang="en">

  {{! Head }}
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>User's List</title>
    <link
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
    <style>
      /* Dark Mode Styling */ body { background-color: #121212; color: #fff; }
      .card { background-color: #1e1e1e; color: #fff; } .modal-content {
      background-color: #1e1e1e; color: #fff; }
    </style>
  </head>

  {{! Body }}
  <body>
    {{{body}}}
  </body>

  {{! Scripts }}
  <script
    src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
    integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
    crossorigin="anonymous"
  ></script>
  <script
    src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
    integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
    crossorigin="anonymous"
  ></script>
  <script>
     $(document).ready(function () {
        $('#users_container').on('click', '.edit',
            function () {
                $('#editUserModal').modal('show');
                $('.id').val($(this).data('id')); 
                $('.name').val($(this).data('name'));
                $('.age').val($(this).data('age'));
                $('.address').val($(this).data('address'));
            }).on('click', '.remove',
              function () {
                $('#removeUserModal').modal('show');
                $('.user_id_removal').val($(this).data('id'));
            });
    });
  </script>

</html>
  • In the user.handlebars, add the following code:
<div class="container" id="users_container">
  <div class="py-5 text-center">
    <h2 class="mb-3">User's List</h2>
    <p class="lead mx-75">CRUD functionality implemented using Node.js, Express,
      Handlebars, and gRPC.</p>
  </div>

  <div class="row">
    {{#each results}}
      <div class="col-md-4 mb-4">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">User ID: {{id}}</h5>
            <h6 class="card-subtitle mb-2 text-muted">User Name:
              {{name}}</h6>
            <p class="card-text">Age: {{age}} years old</p>
            <p class="card-text">Address: {{address}}</p>
            <div class="btn-group" role="group">
              <button
                type="button"
                class="btn btn-primary edit"
                data-toggle="modal"
                data-target="#editUserModal"
                data-id="{{id}}"
                data-name="{{name}}"
                data-age="{{age}}"
                data-address="{{address}}"
              >Edit</button>
              <button
                type="button"
                class="btn btn-danger remove"
                data-id="{{id}}"
                data-toggle="modal"
                data-target="#removeUserModal"
              >Remove</button>
            </div>
          </div>
        </div>
      </div>
    {{else}}
      <div class="col-12 text-center">
        <p>No data to display.</p>
      </div>
    {{/each}}
  </div>

  <button
    class="btn btn-success float-right my-4"
    data-toggle="modal"
    data-target="#newUserModal"
  >Add New</button>
</div>

<!-- New User Modal -->
<form action="/save" method="post">
  <div class="modal fade" id="newUserModal" role="dialog">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title">New User</h4>
          <button type="button" class="close" data-dismiss="modal">
            <span>&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <input
              type="text"
              name="name"
              class="form-control"
              placeholder="User Name"
              required="required"
            />
          </div>
          <div class="form-group">
            <input
              type="number"
              name="age"
              class="form-control"
              placeholder="Age"
              required="required"
            />
          </div>
          <div class="form-group">
            <input
              type="text"
              name="address"
              class="form-control"
              placeholder="Address"
              required="required"
            />
          </div>
        </div>
        <div class="modal-footer">
          <button
            type="button"
            class="btn btn-secondary"
            data-dismiss="modal"
          >Close</button>
          <button type="submit" class="btn btn-primary">Create</button>
        </div>
      </div>
    </div>
  </div>
</form>

<!-- Edit User Modal -->
<form action="/update" method="post">
  <div class="modal fade" id="editUserModal" role="dialog">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title">Edit User</h4>
          <button type="button" class="close" data-dismiss="modal">
            <span>&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <input
              type="text"
              name="name"
              class="form-control name"
              placeholder="User Name"
              required="required"
            />
          </div>
          <div class="form-group">
            <input
              type="number"
              name="age"
              class="form-control age"
              placeholder="Age"
              required="required"
            />
          </div>
          <div class="form-group">
            <input
              type="text"
              name="address"
              class="form-control address"
              placeholder="Address"
              required="required"
            />
          </div>
          <div class="form-group">
            <input type="hidden" name="id" class="id" />
          </div>
        </div>
        <div class="modal-footer">
          <button
            type="button"
            class="btn btn-secondary"
            data-dismiss="modal"
          >Close</button>
          <button type="submit" class="btn btn-primary">Update</button>
        </div>
      </div>
    </div>
  </div>
</form>

<!-- Remove User Modal -->
<form action="/remove" method="post">
  <div
    class="modal fade"
    id="removeUserModal"
    role="dialog"
    aria-labelledby="removoeUserModal"
  >
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title">Remove User</h4>
          <button type="button" class="close" data-dismiss="modal">
            <span>&times;</span>
          </button>
        </div>
        <div class="modal-body">
          Are you sure?
        </div>
        <div class="modal-footer">
          <input
            type="hidden"
            name="user_id"
            class="form-control user_id_removal"
            required="required"
          />
          <button
            type="button"
            class="btn btn-secondary"
            data-dismiss="modal"
          >Close</button>
          <button type="submit" class="btn btn-primary">Remove</button>
        </div>
      </div>
    </div>
  </div>
</form>

Step 6: Run and Test the Application

To Start the Server To start your gRPC server, use the following command:

npm start

Your server should now be running at http://127.0.0.1:30043.

To Start the Client, use the following command:

npm run startClient

Navigate to http://localhost:3000/ and you'll see the interface below:

And that's it, congratulations! 🎉🎉
You can access the full source code by following this link: gRPC Practice GitHub Repository

In summary...

This article provides a concise overview of gRPC, its features and a practical example. To delve deeper into gRPC, you can explore the official documentation here.

I hope you found this helpful.

Thank you for reading!