What is gRPC and why you should care

Asuarezaceves
7 min readAug 9, 2023

--

gRPC (gRPC Remote Procedure Calls) is an open-source framework developed by Google that allows different applications to communicate with each other. It’s a bit like a more advanced form of making a phone call between different parts of a computer program, or between different computers altogether. Here’s how it works:

  1. Protocols and Communication: gRPC uses HTTP/2 for transport, which means it’s designed to be more efficient and can send multiple pieces of information at the same time. It’s like having several lines of communication open at once instead of just one.
  2. HTTP/2 is important because it reduces the travels that the client and server need to communicate with each other or back-and-forth communication between the client and server, making the whole process more efficient. In this image, http2 vs http1 a simple comparison can be found.
  1. Defining Methods: With gRPC, you define methods that can be called remotely in a special language called Protocol Buffers. Think of this like defining the “rules” of the conversation. Both sides need to understand these rules to talk to each other.
  1. Strongly Typed Interface: related to the above, gRPC requires both the client and server to agree on the data structure beforehand. This ensures that both sides know exactly what kind of information is being sent and received. It’s like both parties agreeing on the same language.
  2. Request and Response: The client sends a request to the server and waits for a response. This can be a simple one-to-one request and response, or more complex patterns like streaming, where multiple requests and responses are sent back and forth continuously.
  3. Language Agnostic: gRPC can be used with various programming languages, meaning that a program written in Python could communicate with a program written in Java without any issue.
Lang agnostic
  1. Deadlines/Timeouts: gRPC allows setting deadlines or timeouts, which can be useful to ensure that a request doesn’t take too long to get a response.
  2. Secure Communication: It offers options for secure communication, making sure that the information exchanged between the two parties is private and safe.
  3. Error Handling: gRPC provides detailed error codes, making it easier to understand what went wrong if something doesn’t work as expected.

Below is a simple example that demonstrates using gRPC with Protocol Buffers in Python.

First, you’ll need to define the protocol buffer using a .proto file. Here’s a simple example:

hello.proto:

syntax = "proto3";

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
}

This definition specifies a simple service named HelloService, which has a single method SayHello. The method takes a HelloRequest and returns a HelloResponse.

On to Python:

Let’s install grpcio and grpcio-tools.

pip install grpcio grpcio-tools

In the terminal, run:

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

This will create 2 python files.

  • hello_pb2_grpc
  • hello_pb2

Let’s dive into them.

1. hello_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: hello.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()



DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bhello.proto\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"!\n\rHelloResponse\x12\x10\n\x08greeting\x18\x01 \x01(\t29\n\x0cHelloService\x12)\n\x08SayHello\x12\r.HelloRequest\x1a\x0e.HelloResponseb\x06proto3')


_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hello_pb2', _globals)

if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_HELLOREQUEST']._serialized_start=15
_globals['_HELLOREQUEST']._serialized_end=43
_globals['_HELLORESPONSE']._serialized_start=45
_globals['_HELLORESPONSE']._serialized_end=78
_globals['_HELLOSERVICE']._serialized_start=80
_globals['_HELLOSERVICE']._serialized_end=137
# @@protoc_insertion_point(module_scope)

This file contains the contents for the messages defined in the .proto file. In our example, the HelloRequest and HelloResponse messages are defined in this file. The file also includes code to serialize (convert to a binary format) and deserialize (convert back to Python objects) these messages. Here’s a breakdown of its main components:

  • Serialization and Deserialization: The file includes methods to serialize the messages into a binary format suitable for sending over a network, and to deserialize the binary data back into Python objects.
  • Building Messages and Enum Descriptors: The _builder.BuildMessageAndEnumDescriptors and _builder.BuildTopDescriptorsAndMessages functions are called to construct Python objects representing the messages and services. These objects provide the functionality to serialize and deserialize the messages, among other things.

2. hello_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 hello_pb2 as hello__pb2

class HelloServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.SayHello = channel.unary_unary(
'/HelloService/SayHello',
request_serializer=hello__pb2.HelloRequest.SerializeToString,
response_deserializer=hello__pb2.HelloResponse.FromString,
)
class HelloServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def SayHello(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def add_HelloServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'SayHello': grpc.unary_unary_rpc_method_handler(
servicer.SayHello,
request_deserializer=hello__pb2.HelloRequest.FromString,
response_serializer=hello__pb2.HelloResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'HelloService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))

# This class is part of an EXPERIMENTAL API.
class HelloService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def SayHello(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, '/HelloService/SayHello',
hello__pb2.HelloRequest.SerializeToString,
hello__pb2.HelloResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

This file contains the Python classes for the gRPC services and methods defined in the .proto file. In our example, this includes the HelloService service and its SayHello method. Here’s a breakdown of its main components:

  • Service Classes: The file defines a Python class for each service in the .proto file, such as HelloService.
  • Stub Classes: The file includes a “stub” class for each service, which acts as a client for that service. The client code uses this stub to call the service’s methods.
  • Method Functions: The file defines functions for each method in the .proto file, such as SayHello. These functions handle calling the method over gRPC, including serialization and deserialization of the messages.
  • Server-Side Functionality: The file includes code to help implement the server-side functionality of the services, such as receiving requests, calling the corresponding Python functions, and sending responses.

Let’s run Hello World 😎

First, let’s create 2 files. A Server, and a Client.

# server.py
import grpc
import hello_pb2
import hello_pb2_grpc
import concurrent.futures

class HelloService(hello_pb2_grpc.HelloServiceServicer):
def SayHello(self, request, context):
return hello_pb2.HelloResponse(greeting=f"Hello, {request.name}!")

def serve():
server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=10))
hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()

if __name__ == '__main__':
serve()
# client.py
import grpc
import hello_pb2
import hello_pb2_grpc

def run():
channel = grpc.insecure_channel('localhost:50051')
stub = hello_pb2_grpc.HelloServiceStub(channel)
response = stub.SayHello(hello_pb2.HelloRequest(name='World'))
print(response.greeting)

if __name__ == '__main__':
run()

in 2 different terminals, run the python files.

python server.py

Then in another termial run:

python client.py

You will get: Hello, World! As expected and defined in our code :)

What we essentially just did, was simulate communcation between a server and a client.

In a few short steps, we were able to implement a reliable, replicable interaction:

Define the Protocol (.proto file):

  • You create a .proto file that defines the service, methods, and message types.
  • This file acts as a contract between the client and server, specifying the structure of the data and the available operations.
  • You compile this .proto file using grpc_tools to generate Python code (hello_pb2.py and hello_pb2_grpc.py), which includes everything needed to implement the server and client.

Implement the Server:

  • In a Python file (e.g., server.py), you import the generated code and create a class that implements the service defined in the .proto file.
  • You start a gRPC server, register the service implementation, and specify the address and port for the server to listen on.
  • The server is now ready to receive client requests, execute the corresponding methods, and send back responses.

Implement the Client:

  • In another Python file (e.g., client.py), you import the generated code and create a client (or “stub”) for the service defined in the .proto file.
  • You connect the client to the server’s address and port, call the desired method (e.g., SayHello), and handle the response (e.g., print it).
  • The client sends the request to the server, receives the response, and can make additional calls as needed.

Run the Simulation:

  • First, you run the server code (e.g., python server.py). It starts listening for client connections.
  • Then, in another terminal, you run the client code (e.g., python client.py). It connects to the server, makes the request, and receives the response.
  • You observe the interaction between the client and server, including the data sent and received.

Conclusion:

gRPC is a modern, high-performance RPC framework that uses HTTP/2 for efficient communication. It allows for strongly-typed, language-agnostic communication between services, facilitating the development of scalable and maintainable systems.

By defining clear contracts through Protocol Buffers, gRPC ensures consistent interfaces, streamlines development, and enhances collaboration. Its support for bi-directional streaming and multiplexing makes it a robust choice for contemporary distributed systems. Adopting gRPC can lead to more efficient and reliable inter-service communication, making it a valuable tool for developers and businesses alike.

--

--