What is Google Remote Procedure Call (gRPC) and how does gRPC work?

How does gRPC streamline development for distributed applications?

What is gRPC?

Google Remote Procedure Call, (gRPC), is an open-source remote procedure call framework developed by Google. To communicate, clients and servers use gRPC and define a service contract via a language-agnostic binary serialization format called Protocol Buffers, or protobuf..

We can use gRPC to build high-performance distributed systems.

How does gRPC work?

In gRPC, we describe the service interface by defining a set of functions that can be called remotely, and then generate the initial code skeleton for both the server and the client.

The generated server code is then completed with business-logic code as needed. Similarly, the generated client code is used to call server functions as if they are regular functions, without having to deal with the complexities of network communication.

This approach has become popular in microservice architectures and is widely used in cloud-native environments. It also has some features that make it very attractive to developers.

Language independence

Because gRPC is language agnostic , we can implement clients and servers in different programming languages and these implementations will work together seamlessly. This gives us flexibility and interoperability.

This is possible because gRPC has libraries and bindings for many programming languages, including C++, Java, Python, Go, and many others.

This interoperability and multi-language support is possible because gRPC uses Protocol Buffers: a language- and platform-neutral mechanism that allows us both to describe the service interface and specify the message format.

Code generation

In gRPC, we generate the initial client and server code from the service definition specified in a .proto file. The generated code provides type-safe APIs and abstracts away the complexity of network communication.

Authentication and load balancing

GRPC has built-in support for various authentication mechanisms and load balancing strategies. This makes it suitable for building scalable and secure distributed systems.

Bidirectional streaming

GRPC supports both unary (request-response) and streaming communication. It allows clients and servers to establish bidirectional streaming channels to send and receive multiple messages asynchronously. Such bidirectional streaming uses network resources efficiently because it enables concurrent sending and receiving of messages without waiting for a request-response cycle to complete.

This is well-suited for scenarios where real-time, interactive, and continuous communication is required, such as chat applications, real-time collaboration tools, or data streaming applications.

An example application

Let’s build a simple to-do application.

First, we’ll specify the service interface and the format of exchanged messages. Then we’ll generate the initial server and client code. Finally, we’ll fill the generated code with required business logic.

While the server and the client could be implemented in different programming languages, we’ll use Python for both to keep this example simple.

Describe the service interface with a .proto file

To start, we describe the service interface and the message types in the file todo.proto.

The service interface has a set of functions that the server offers and clients can call. The message types define different input and output messages that these functions use.

syntax = "proto3";

package todo;

service TodoService {
  rpc AddTodo (Item) returns (Item) {}
  rpc GetTodo (TodoRequest) returns (Item) {}
  rpc MarkCompleted (TodoRequest) returns (Item) {}
  rpc ListTodos (ListTodosRequest) returns (stream Item) {}
}

message Item {
  int32 id = 1;
  string name = 2;
  bool completed = 3;
  string timestamp = 4;
}

message TodoRequest {
  int32 id = 1;
}

message ListTodosRequest {
  int32 skip = 1;
  int32 offset = 2;
}

Inside the service block we specify functions:

  • AddTodo
  • GetTodo
  • MarkCompleted
  • ListTodos

The functions specify input parameters, their types, and the return values and corresponding types. The rpc keyword precedes each function.

Below the service block we have three message blocks that define required messages. For instance, a to-do Item consists of:

  • an id type int32
  • a name type string
  • a bool denoting whether the task has been completed
  • the timestamp denoting when the task was created as type google.protobuf.Timestamp.

TodoRequest represents a data structure that wraps an id field which is used to query a single to-do item or mark a given item as completed, and similarly the ListTodoRequest allows querying a list of items where the client can specify additional query parameters.

The integer values assigned to these fields, for instance, int32 id = 1; denote a unique tag used by the Protocol Buffers binary encoding. While it may appear that these are default values, they are not. These numbers must be unique within each message, and effectively represent fields’ binary names. Changing the value is the same as deleting a field and adding a new one.

For more information regarding message definitions, see the official Protocol Buffers documentation.

Generate the initial code

Once we have specified the service interface, we can generate the initial code skeleton. For this, we need a protoc compiler for the targeted language. Since we’re using Python, we can install the compiler with pip.

$ pip install grpcio grpcio-tools

Then we can run the protoc compiler as follows:

$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. todo.proto

This should generate two files, todo_pb2.py todo_pb2_grpc.py. The first contains the generated request and response classes and the other contains generated client and server classes.

Implement the server code

Now we have all the required utilities to implement the server and the client. The server implementation in server.py should contain the following code:

import sys
from concurrent import futures

import grpc

import todo_pb2_grpc


class TodoService(todo_pb2_grpc.TodoServiceServicer):
    todos = []    # 'Database' of TODOs
    _counter = 0  # Generates unique ids.

    def AddTodo(self, request, context):
        """Adds a new TODO to the list"""
        self._counter += 1

        todo = request
        todo.id = self._counter
        todo.timestamp.GetCurrentTime()
        self.todos.append(todo)
        return todo

    def GetTodo(self, request, context):
        """Returns a TODO for a given id, error otherwise"""
        for todo in self.todos:
            if todo.id == request.id:
                return todo

        context.set_code(grpc.StatusCode.NOT_FOUND)
        context.set_details("Todo not found.")

    def ListTodos(self, request, context):
        """Returns a stream of TODOs, where optional parameters skip and
        offset can be used"""
        skip = request.skip if request.skip else 0
        offset = request.offset if request.offset else sys.maxsize
        for todo in self.todos[skip:skip + offset]:
            yield todo

    def MarkCompleted(self, request, context):
        """Marks a given TODO as completed"""
        for todo in self.todos:
            if todo.id == request.id:
                todo.completed = True
                return todo

        context.set_code(grpc.StatusCode.NOT_FOUND)
        context.set_details("Todo not found.")


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    todo_pb2_grpc.add_TodoServiceServicer_to_server(TodoService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("Server started. Listening on port 50051...")
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

Class TodoService implements the gRPC service’s methods, such as AddTodo and GetTodo. These are simple Python methods that process request and context parameters. The request denotes any service input parameters, and context provides RPC-specific information, like timeout limits.

The AddTodo method reads the incoming TODO item from the variable request, sets a unique id and the current date and time, and appends the TODO to the database.

The GetTodo method shows how to use the context parameter: when the client requests an item without an id, we use context to set the error code to NOT_FOUND. (In a RESTful service, we would return a 404 not found response code.) We’ll implement the remaining two service functions in a similar way.

Lastly, to start the server, we implement the method serve() and call it when the server.py file runs. The method instantiates a gRPC server on a given port and specifies that it will offer services from class TodoService.

To start the server, open a terminal and run the following command.

$ python server.py

Implement the client code

Now we just need to implement the client code. The client code depends on which actions the client wishes to make.

In this example, we:

  1. Add a TODO item to the database
  2. Query the newly added TODO item
  3. Mark the newly created item as complete
  4. Select all TODO items from the database

The client.py is very straightforward.

import grpc
from grpc import RpcError

import todo_pb2
import todo_pb2_grpc


def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = todo_pb2_grpc.TodoServiceStub(channel)

    # Add a new TODO item
    todo = todo_pb2.Item(name="Hello World! This is a TODO!")
    todo = stub.AddTodo(todo)
    print(f"Added Todo:", todo.id, todo.name, todo.completed, todo.timestamp.ToDatetime())
    print()

    # Get a TODO item by ID
    try:
        todo = stub.GetTodo(todo_pb2.TodoRequest(id=1))
        print(f"Got Todo for id = 1:", todo.id, todo.name, todo.completed, todo.timestamp.ToDatetime())
    except RpcError as e:
        print("Error occurred while getting Todo:", e)

    # Mark a TODO completed
    try:
        todo = stub.MarkCompleted(todo_pb2.TodoRequest(id=1))
        print(f"Got Todo for id = 1:", todo.id, todo.name, todo.completed, todo.timestamp.ToDatetime())
    except RpcError as e:
        print("Error occurred while getting Todo:", e)

    # List all TODO items
    list_response = stub.ListTodos(todo_pb2.ListTodosRequest(skip=0))
    print()
    print("List Todos:")
    for todo in list_response:
        print(todo.id, todo.name, todo.completed, todo.timestamp.ToDatetime())


if __name__ == '__main__':
    run()

First, we connect to the server through a channel and then instantiate a stub to call service functions from the server.

Next, we instantiate a new TODO item. We add it to the database by calling the stub.AddTodo(todo). Behind the scenes, the stub takes the Python Item object, serializes it to wire-format, sends it to the server where it is deserialized and given a fresh id and current timestamp, and then saved to the database.

We can take a similar approach with other queries. For instance, if we query a TODO item based on its id, we mark a TODO item complete with the method MarkCompleted.

We can also list all TODO items in the database with the stub.ListTodos(ListTodosRequest). In the latter case, the service returns a stream of TODO items. A stream is like a list where the contents can come with a delay. The method takes in a ListTodoRequest object that may contain two fields: skip and offset. These two fields support pagination and work like the LIMIT command in SQL.

Results

Let’s run the client.

$ python client.py
python client.py
Added Todo: 1 Hello World! This is a TODO! False 2023-07-14 12:33:56.241896

Got Todo for id = 1: 1 Hello World! This is a TODO! False 2023-07-14 12:33:56.241896
Got Todo for id = 1: 1 Hello World! This is a TODO! True 2023-07-14 12:33:56.241896

List Todos:
1 Hello World! This is a TODO! True 2023-07-14 12:33:56.241896

Conclusion

Notice how a lot of things happened under the hood: - We didn’t have to install and run a dedicated HTTP server. - We didn’t have to decide on a message serialization format. - We didn’t explicitly serialize or deserialize messages.

The service description file generated the service interface. The initial client libraries were generated for us, and we can invoke remote functions as if they were local to us.

Because the development experience is so streamlined, gRPC is becoming more prevalent in developing distributed applications.

Did you find this article helpful?

0 out of 0 Bunnies found this article helpful

Glossary

HTTP

Hypertext Transfer Protocol. A protocol that connects web browsers to web servers when they request content.

Prove your Knowledge.
Earn a bunny diploma.

Test your knowledge in our Junior and Master Quizes to see where you stand. Prove your mastery by getting A+ and recieving a diploma.

Start the QuizBunny with a diploma.
Cookie message
We use cookies to improve your user experience. Learn more
x
Start a chat!