gRPC

Posted by neverset on January 6, 2021

gRPC stands for Remote Procedure Call (RPC) protocol, that leverages Protocol Buffers (protobufs) as its message format. the client application can directly call method available on a remote server using method stubs. Protobufs are alternative to formats like JSON or XML, which is smaller, simpler and more efficient way of serializing data. advantages over restful api:

  • reduces the size of payloads being sent
  • use HTTP/2: concurrent requests, streaming instead of request-response, smaller sensitivity to latency
  • gRPC contracts are stricter and clearly defined it fits especially good for IoT, mobile devices or other constrained/low-power environments. disadvantages are: Not all clients (browsers) support the use of HTTP/2

an boilerplate for gRPC is https://github.com/MartinHeinz/python-project-blueprint

components

protocol

an example protocol is like this:

// example.proto
syntax = "proto3";

package example;

message User {
int32 id = 1;
string name = 2;
}

message UserInfo {
int32 age = 1;
string address = 2;
string phoneNumber = 3;
}

methods

service UserService {
    rpc GetUserInfo (User) returns (UserInfo) {}
}

Installation and Setting Up

#! /bin/bash
# Download and Unzip compiler
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
unzip protoc-3.11.4-linux-x86_64.zip -d protoc3

# Move the binary to directory which is PATH
sudo mv protoc3/bin/* /usr/local/bin/

sudo mv protoc3/include/* /usr/local/include/

# Change owner
sudo chown $USER /usr/local/bin/protoc
sudo chown -R $USER /usr/local/include/google

# Test if it works
protoc --version
# libprotoc 3.11.4

#activate python environment
source .../venv/bin/activate
pip install grpcio grpcio-tools

usage

server

to start the server you need to run: python -m blueprint

build server

// put these in the protocol file echo.proto
syntax = "proto3";
package echo;

// The request message containing the user's message.
message EchoRequest {
string message = 1;
}

// The response message containing the original message.
message EchoReply {
string message = 1;
}

// The echo service definition.
service Echo {
// Echo back reply.
rpc Reply (EchoRequest) returns (EchoReply) {}
}

to use these protocol definition we need server and client interfaces:

python3 -m grpc_tools.protoc \
        #specify output generated *_pb2.py and *_grpc_pb2.py files respectively.
        --python_out=./blueprint/generated \
        --grpc_python_out=./blueprint/generated \
        #specify protoscol file path
        ./blueprint/proto/*.proto

sed -i -E 's/^import.*_pb2/from . \0/' ./blueprint/generated/*.py

utilize generated service

# grpc.py
from .generated import echo_pb2_grpc, echo_pb2
class Echoer(echo_pb2_grpc.EchoServicer):
    def Reply(self, request, context):
        return echo_pb2.EchoReply(message=f'You said: {request.message}')

application server

# app.py
from concurrent import futures
import grpc
from .generated import echo_pb2_grpc
from .grpc import Echoer

class Server:
    @staticmethod
    def run():
        server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
        echo_pb2_grpc.add_EchoServicer_to_server(Echoer(), server)
        server.add_insecure_port('[::]:50051')
        server.start()
        server.wait_for_termination()

# __main__.py
from .app import Server
if __name__ == '__main__':
    Server.run()

client

to run client: $ python -m blueprint.echo_client

from __future__ import print_function
import logging

import grpc

from .generated import echo_pb2
from .generated import echo_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = echo_pb2_grpc.EchoStub(channel)
        response = stub.Reply(echo_pb2.EchoRequest(message='Hello World!'))
    print("Echo client received: " + response.message)

if __name__ == '__main__':
    logging.basicConfig()
    run()

pytest

# conftest.py
import pytest

@pytest.fixture(scope='module')
def grpc_add_to_server():
    from blueprint.generated.echo_pb2_grpc import add_EchoServicer_to_server
    return add_EchoServicer_to_server

@pytest.fixture(scope='module')
def grpc_servicer():
    from blueprint.grpc import Echoer
    return Echoer()

@pytest.fixture(scope='module')
def grpc_stub(grpc_channel):
    from blueprint.generated.echo_pb2_grpc import EchoStub
    return EchoStub(grpc_channel)

# test_grpc.py
def test_reply(grpc_stub):
    value = 'test-data'
    request = blueprint.echo_pb2.EchoRequest(message=value)
    response = grpc_stub.Reply(request)
    assert response.message == f'You said: {value}'