Skip to content
DEV
Go back

Getting started with gRPC

Table of contents

What is RPC?

Before we dive into gRPC, it is important that we understand what RPC is first. RPC stands for Remote Procedure Calls. RPC allows us to invoke methods on a remote server as if they were being invoked locally. There are many frameworks that implement RPC. Some of them are Twirp, gRPC, Apache Thrift and Apache Avro. Among them, gRPC is one of the most widely adopted RPC frameworks.

What is gRPC?

gRPC (gRPC Remote Procedure Calls) is a high performance RPC framework developed by Google. It uses Protocol Buffers for serializing structured data and HTTP/2 as its network protocol. Protocol Buffers serializes data into binary format, making the messages much more compact in comparison to JSON. Small sized messages reduce bandwidth usage and can improve latency. gRPC heavily relies on HTTP/2 to provide high performance and efficiency. Features of HTTP/2 like multiplexing, streaming(bi-directional) and header compression(HPACK) make gRPC one of the most efficient RPC frameworks.

gRPC is language agnostic, making it suitable for teams where each team uses a different tech stack and follow distributed architecture.

gRPC vs REST

REST uses JSON over HTTP/1.1, which makes it human-readable but verbose. gRPC uses Protocol Buffers to send binary serialized data over HTTP/2, making it very efficient and performant. gRPC shines where high performance and low latency are required.

Building a gRPC Application

Defining the Service (.proto)

To build a gRPC application, we first need to define a .proto file. A .proto file defines a service, the methods belonging to the service, and the request and response message types. It defines a strict contract between client and server.

Below is an example of .proto file.

// name file example.proto

syntax = "proto3"; // version of protobuf being used

package example; // package name

// Creating a service and defining its methods
service ExampleService {
  // RPC method
  rpc ExampleMethod(ExampleMethodRequest) returns (ExampleMethodResponse);
}

// Defining the method input format
message ExampleMethodRequest {
  string name = 1;
}

// Defining the method output format
message ExampleMethodResponse {
  string resp_str = 1;
}

Generating the Code

Once the .proto file is created, we can use the protocol buffers compiler to generate code in the language of our choice. We will be using the gRPC plugin for python to generate the code(install plugins by running pip install grpcio grpcio-tools). Running the below command will generate the python glue required to send and receive via gRPC using python.

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. example.proto

This command will generate two files: example_pb2.py and example_pb2_grpc.py, respectively. The above command needs to be run every time the .proto file is updated.

example_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: example.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
    _runtime_version.Domain.PUBLIC,
    6,
    31,
    1,
    '',
    'example.proto'
)
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()


DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rexample.proto\x12\x07\x65xample\"$\n\x14\x45xampleMethodRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\")\n\x15\x45xampleMethodResponse\x12\x10\n\x08resp_str\x18\x01 \x01(\t2`\n\x0e\x45xampleService\x12N\n\rExampleMethod\x12\x1d.example.ExampleMethodRequest\x1a\x1e.example.ExampleMethodResponseb\x06proto3')

_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
  DESCRIPTOR._loaded_options = None
  _globals['_EXAMPLEMETHODREQUEST']._serialized_start=26
  _globals['_EXAMPLEMETHODREQUEST']._serialized_end=62
  _globals['_EXAMPLEMETHODRESPONSE']._serialized_start=64
  _globals['_EXAMPLEMETHODRESPONSE']._serialized_end=105
  _globals['_EXAMPLESERVICE']._serialized_start=107
  _globals['_EXAMPLESERVICE']._serialized_end=203
# @@protoc_insertion_point(module_scope)

This file contains Python bindings for the message definitions in the .proto file.

example_pb2_grpc.py

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings

import example_pb2 as example__pb2

GRPC_GENERATED_VERSION = '1.80.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

try:
    from grpc._utilities import first_version_is_lower
    _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
    _version_not_supported = True

if _version_not_supported:
    raise RuntimeError(
        f'The grpc package installed is at version {GRPC_VERSION},'
        + ' but the generated code in example_pb2_grpc.py depends on'
        + f' grpcio>={GRPC_GENERATED_VERSION}.'
        + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
        + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
    )


class ExampleServiceStub(object):
    """Creating a service and defining its methods
    """

    def __init__(self, channel):
        """Constructor.

        Args:
            channel: A grpc.Channel.
        """
        self.ExampleMethod = channel.unary_unary(
                '/example.ExampleService/ExampleMethod',
                request_serializer=example__pb2.ExampleMethodRequest.SerializeToString,
                response_deserializer=example__pb2.ExampleMethodResponse.FromString,
                _registered_method=True)


class ExampleServiceServicer(object):
    """Creating a service and defining its methods
    """

    def ExampleMethod(self, request, context):
        """RPC method
        """
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')


def add_ExampleServiceServicer_to_server(servicer, server):
    rpc_method_handlers = {
            'ExampleMethod': grpc.unary_unary_rpc_method_handler(
                    servicer.ExampleMethod,
                    request_deserializer=example__pb2.ExampleMethodRequest.FromString,
                    response_serializer=example__pb2.ExampleMethodResponse.SerializeToString,
            ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
            'example.ExampleService', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))
    server.add_registered_method_handlers('example.ExampleService', rpc_method_handlers)


# This class is part of an EXPERIMENTAL API.
class ExampleService(object):
    """Creating a service and defining its methods
    """

    @staticmethod
    def ExampleMethod(request,
            target,
            options=(),
            channel_credentials=None,
            call_credentials=None,
            insecure=False,
            compression=None,
            wait_for_ready=None,
            timeout=None,
            metadata=None):
        return grpc.experimental.unary_unary(
            request,
            target,
            '/example.ExampleService/ExampleMethod',
            example__pb2.ExampleMethodRequest.SerializeToString,
            example__pb2.ExampleMethodResponse.FromString,
            options,
            channel_credentials,
            insecure,
            call_credentials,
            compression,
            wait_for_ready,
            timeout,
            metadata,
            _registered_method=True)

This file contains gRPC Python glue code used to create a client (stub) and define a server interface for the service described in the .proto file.

Building the Server and Client

Once the files are generated, we can start creating a client and a server.

server.py

# pooling to handle multiple requests
from concurrent import futures

import grpc
import example_pb2
import example_pb2_grpc


# implementation of the service.
class ExampleService(example_pb2_grpc.ExampleServiceServicer):
    def ExampleMethod(self, request, context):
        return example_pb2.ExampleMethodResponse(resp_str=f"hello {request.name}!")


# running the service at port 50051
def main():
    # Thread pool to handle multiple concurrent RPC requests
    server = grpc.server(futures.ThreadPoolExecutor(10))

    # Implements the gRPC service defined in the .proto file
    example_pb2_grpc.add_ExampleServiceServicer_to_server(ExampleService(), server)
    server.add_insecure_port("localhost:50051")

    # starting the server.
    server.start()
    print("Server started")
    server.wait_for_termination()


main()

This is the server. This will receive requests from the clients and it will respond to them.

client.py

import grpc

import example_pb2
import example_pb2_grpc


def main():
    channel = grpc.insecure_channel("localhost:50051")

    # A generated client-side proxy that exposes remote methods as local function calls.
    stub = example_pb2_grpc.ExampleServiceStub(channel)

    # Makes a remote RPC call to ExampleMethod on the server
    response = stub.ExampleMethod(example_pb2.ExampleMethodRequest(name="john doe"))
    print(response.resp_str)


main()

This is the client that sends request to the server.

Running the Application

To run, open two terminals:

Terminal 1: python3 server.py Terminal 2: python3 client.py

Expected output: hello john doe!

Types of RPC Calls

The code above is an example of Unary RPC. In gRPC, there are four types of RPC calls:

  1. Unary RPC - The client sends a single request, and the server responds with a single message.
  2. Server Side Streaming - Client sends a single request, and the server responds with a stream of messages.
  3. Client Side Streaming - The client sends a stream of requests, and the server responds with a single message after receiving all of them.
  4. Bidirectional Streaming - Both the client and server send streams of messages to each other independently without blocking.

Trade-offs

gRPC is powerful, but it does come with some trade-offs.

  1. Messages are not human-readable. Since the data is binary encoded, it is not possible to easily read it like JSON, making it difficult to debug.
  2. Browsers don’t natively support gRPC. To use gRPC in browsers, a workaround like gRPC-Web is required.

Despite these trade-offs, gRPC is an excellent choice for internal service-to-service communication, especially where performance and low latency matter.

References


Share this post:

Previous Post
Report Generation Using Typst: Faster PDF Rendering in Python.
Next Post
Jev: TypeSafe's decision model that isn't an LLM