gRPC for Python: The Next-Level Communication Protocol for Distributed Systems and microservices

10 min read

Introduction

In today's rapidly evolving world of software development, building scalable and efficient distributed systems and microservices has become crucial for businesses to stay competitive. One of the key challenges in this realm is establishing seamless communication between various components of the system. Enter gRPC - a powerful communication protocol that has been gaining significant traction, especially when combined with Python.

In this blog, we will delve into the world of gRPC for Python and explore how it unlocks the potential of distributed systems and microservices. We'll cover the fundamental concepts, benefits, and provide practical examples of using gRPC to create high-performance APIs.

1. Understanding gRPC

To get started, let's grasp the core concepts of gRPC. gRPC is an open-source, high-performance Remote Procedure Call (RPC) framework developed by Google. It enables efficient communication between different services running on various platforms and languages. The protocol uses Protocol Buffers (protobufs) as its Interface Definition Language (IDL), allowing for language-agnostic and compact data serialization.

2. Why Choose gRPC for Python?

Python is a versatile and widely-used programming language, known for its simplicity and readability. When combined with gRPC, Python becomes an excellent choice for building distributed systems and microservices. Some reasons to choose gRPC for Python are:

  1. Performance: gRPC utilizes HTTP/2, a multiplexed and binary protocol that reduces latency and improves data transmission speed, resulting in faster communication between services.
  2. Language-agnostic: Since gRPC uses protobufs, it allows services written in different programming languages to communicate seamlessly. This means you can have services written in Python communicating with ones written in Java, C++, Go, and more.
  3. Streaming Support: gRPC supports bidirectional streaming, enabling real-time communication and data flow between services, which is beneficial for applications that require continuous data exchange.
  4. Automatic Code Generation: gRPC generates client and server code based on the defined service contract in the .proto files, making it easier to maintain and less error-prone.
3. gRPC in Action: Building Microservices with Python

Let's dive into a practical example to see gRPC in action. We'll create a simple microservice architecture consisting of two services: a client that sends requests to a server, which in turn responds with the requested data.

  1. Defining the Service Contract with Protobufs: We'll start by defining the service and its methods in a .proto file using protobuf syntax. This contract will be used to generate Python code for the client and server.
  2. Implementing the Server: In this step, we'll write the server code in Python using the generated code from the .proto file. The server will listen for incoming requests, process them, and return the appropriate response.
  3. Creating the Client: We'll then create a Python client that interacts with the server by sending requests and handling responses.
  4. Running the Microservices: With both the server and client implemented, we'll run the microservices and observe how they communicate using gRPC.
4. Securing Communication with gRPC

Security is a paramount concern when dealing with distributed systems. In this section, we'll explore how to secure communications between gRPC services using Transport Layer Security (TLS) encryption.

5. Scaling with gRPC: Load Balancing and Service Discovery

As the number of services grows, load balancing and service discovery become critical components of the system architecture. We'll discuss how gRPC provides built-in support for load balancing and service discovery, making it easier to scale the microservices.

6. gRPC Interoperability: Bridging the Gap Between Different Technologies

In a heterogeneous ecosystem, different technologies may coexist. gRPC offers solutions for interoperability between various frameworks, allowing Python-based services to communicate with non-Python services using the gRPC gateway and proxy.

Lets build now

Required packages i used

dnspython = "*"
grpcio-tools = "*"
pymongo = "*"

here is the basic proto file code

syntax = "proto3";

package unary;

service Unary{
 rpc GetServerResponse(Message) returns (MessageResponse) {}

}

message Message{
 string message = 1;
}

message MessageResponse{
 string message = 1;
 bool received = 2;
}

you can place the file in the protobufs folder

after this you must run the following command after creating the GRPC_PB2 folder

python -m grpc_tools.protoc -I ./protobufs --python_out=./GRPC_PB2 --grpc_python_out=./GRPC_PB2 ./protobufs/datafilter.proto

this will create two files in the GRPC_PB2 folder (datafilter_pb2.py and datafilter_pb2_grpc.py)

now we will create server side file named server.py with the following code.

import json
from concurrent import futures

import grpc
from pymongo import MongoClient

import GRPC_PB2.datafilter_pb2 as pb2
import GRPC_PB2.datafilter_pb2_grpc as pb2_grpc


class UnaryService(pb2_grpc.UnaryServicer):

    def __init__(self, *args, **kwargs):
        pass

    def GetServerResponse(self, request, context):
        # get the string from the incoming request
        message = request.message
        curser = MongoClient('mongodb://localhost:27017/admin')
        student_coll = curser['localdb']['student']
        print(f"==>> message: {message}")
        query = list(student_coll.find({"name":f"{message}"},{"_id":0}))
        query = json.dumps(query) if query else 'No Data Found'
        result = {'message': query, 'received': True}
        print(result)
        return pb2.MessageResponse(**result)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    pb2_grpc.add_UnaryServicer_to_server(UnaryService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("[+] GRPC server stated...")
    server.wait_for_termination()




if __name__ == '__main__':
    serve()

now we create client side code in client.py

import grpc

import GRPC_PB2.datafilter_pb2 as pb2
import GRPC_PB2.datafilter_pb2_grpc as pb2_grpc

# python -m grpc_tools.protoc --proto_path=.  ./datafilter.proto --python_out=. --grpc_python_out=.


class UnaryClient(object):
    """
    Client for gRPC functionality
    """

    def __init__(self):
        self.host = 'localhost'
        self.server_port = 50051
        # instantiate a channel
        self.channel = grpc.insecure_channel(f'{self.host}:{self.server_port}')
        # bind the client and the server
        self.stub = pb2_grpc.UnaryStub(self.channel)

    def get_url(self, message):
        """
        Client function to call the rpc for GetServerResponse
        """
        message = pb2.Message(message=message)
        return self.stub.GetServerResponse(message)


if __name__ == '__main__':
    print('[+] Connecting Student database..')
    while True:
        client = UnaryClient()
        result = client.get_url(message=input("Enter Name: "))
        print(f'{result}')

 

Conclusion

gRPC for Python brings a new dimension to building distributed systems and microservices. Its speed, efficiency, and language-agnostic nature make it a valuable tool for creating high-performance APIs and services. As you venture into the world of gRPC with Python, you'll find a vast community, extensive documentation, and a plethora of resources to support your journey. So why wait? Embrace gRPC and unlock the potential of your distributed applications!