gRPC-Example#

By default, gRPC uses Protocol Buffers (Protobuf) for serialising data, although it also works with other data formats such as JSON.

Define the data structure#

The first step when working with protocol buffers is to define the structure for the data you want to serialise in a .proto file. Protocol buffer data is structured as messages, where each message is a small logical record of information containing a series of name-value pairs called fields. Here’s a simple example accounts.proto:

accounts.proto#
// SPDX-FileCopyrightText: 2021 Veit Schiele
//
// SPDX-License-Identifier: BSD-3-Clause

syntax = "proto3";

message Account {

Warning

You shouldn’t simply use uint32 for user or group IDs, as these would be far too easy to guess. You can use an RFC 4122-compliant implementation for this purpose. You can find a corresponding protobuf configuration in rfc4122.proto.

After you have defined your data structure, you use the protocol buffer compiler protoc to generate descriptors in your preferred languages. These provide simple accessors for each field, as well as methods to serialise the whole structure. For example, if your language is Python, running the compiler on the example above will generate declarators you can then use in your application to populate, serialise, and retrieve protocol buffer messages.

Define the gRPC service#

gRPC services are also defined in the .proto files, with RPC method parameters and return types specified as protocol buffer messages:

accounts.proto#
  uint32 account_id = 1;
  string account_name = 2;
}

message CreateAccountRequest {
  string account_name = 1;
}

message CreateAccountResult {
  Account account = 1;
}

message GetAccountsRequest {
  repeated Account account = 1;
}

message GetAccountsResult {
  Account account = 1;
}

service Accounts {
  rpc CreateAccount (CreateAccountRequest) returns (CreateAccountResult);
  rpc GetAccounts (GetAccountsRequest) returns (stream GetAccountsResult);
}

Generate the gRPC Code#

$ pipenv install grpcio grpcio-tools
$ pipenv run python -m grpc_tools.protoc --python_out=. --grpc_python_out=. accounts.proto

This generates two files:

accounts_pb2.py

contains classes for the messages defined in accounts.proto.

accounts_pb2_grpc.py

contains the defined classes AccountsStub for calling RPCs, AccountsServicer for the API definition of the service and a function add_AccountsServicer_to_server for the server.

Create server#

For this we write the file accounts_server.py:

accounts_server.py#
# SPDX-FileCopyrightText: 2021 Veit Schiele
#
# SPDX-License-Identifier: BSD-3-Clause

import logging
from concurrent import futures

import accounts_pb2 as accounts_messages
import accounts_pb2_grpc as accounts_service
import grpc


class AccountsService(accounts_service.AccountsServicer):
    def CreateAccount(self, request, context):
        metadata = dict(context.invocation_metadata())
        print(metadata)
        account = accounts_messages.Account(
            account_name=request.account_name, account_id=1
        )
        return accounts_messages.CreateAccountResult(account=account)

    def GetAccounts(self, request, context):
        for account in request.account:
            account = accounts_messages.Account(
                account_name=account.account_name,
                account_id=account.account_id,
            )
            yield accounts_messages.GetAccountsResult(account=account)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    accounts_service.add_AccountsServicer_to_server(AccountsService(), server)
    server.add_insecure_port("[::]:8081")
    server.start()
    server.wait_for_termination()


if __name__ == "__main__":
    logging.basicConfig()
    serve()

Create client#

For this we create accounts_client.py:

accounts_client.py#
# SPDX-FileCopyrightText: 2021 Veit Schiele
#
# SPDX-License-Identifier: BSD-3-Clause

import logging

import accounts_pb2 as accounts_messages
import accounts_pb2_grpc as accounts_service
import grpc


def run():
    channel = grpc.insecure_channel("localhost:8081")
    stub = accounts_service.AccountsStub(channel)
    response = stub.CreateAccount(
        accounts_messages.CreateAccountRequest(account_name="tom"),
    )
    print("Account created:", response.account.account_name)


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

Run client and server#

  1. Starting the server:

    $ pipenv run python accounts_server.py
    
  2. Starting the client from another terminal:

    $ pipenv run python accounts_client.py
    Account created: tom