gRPC-Beispiel

Standardmäßig verwendet gRPC ../serialisation-formats/protobuf zur Serialisierung von Daten, obwohl sie auch mit anderen Datenformaten wie JSON funktionieren.

Definieren der Datenstruktur

Der erste Schritt bei der Arbeit mit Protocol-Buffers besteht darin, die Struktur für die Daten zu definieren, die Sie in einer .proto-Datei serialisieren möchten. Protocol-Buffers-Daten sind als Nachrichten strukturiert, wobei jede Nachricht ein kleiner logischer Datensatz ist, der eine Reihe von Name-Wert-Paaren enthält, die fields genannt werden. accounts.proto ist ein einfaches Beispiel hierfür:

syntax = "proto3";

message Account {
  uint32 account_id = 1;
  string account_name = 2;
}

Warnung

Beachtet bitte, dass ihr üblicherweise nicht einfach uint32 für User- oder Group-IDs verwenden solltet, da diese viel zu einfach zu erraten wären. Hierfür könnt ihr z.B. eine RFC 4122-konforme Implementierung verwenden. Eine entsprechende Protobuf-Konfiguration findet ihr in rfc4122.proto.

Nachdem ihr eure Datenstruktur definiert habt, könnt ihr das Protocol-Buffer-Compiler-Protokoll protoc verwenden, um Deskriptoren in eurer bevorzugten Sprache zu erzeugen. Diese bietet einfache Zugriffsfunktionen für jedes Feld sowie Methoden zur Serialisierung der gesamten Struktur. Wenn eure Sprache z.B. Python ist, werden beim Ausführen des Compilers für das obige Beispiel Deklaratoren generiert, die ihr dann in eurer Anwendung zum Einpflegen, Serialisieren und Abrufen von Protocol-Buffer-Nachrichten verwenden könnt.

Definieren des gRPC-Dienstes

gRPC-Dienste werden ebenfalls in den .proto-Dateien definiert, wobei die RPC-Methodenparameter und Rückgabetypen als Protocol-Buffer-Nachrichten angegeben werden:

message CreateAccountRequest {
  string account_name = 1;
}

message CreateAccountResult {
  Account account = 1;
}

message GetAccountsRequest {
  repeated Account account = 1;
}

message GetAccountsResult {
  Account account = 1;
}

Generieren des gRPC-Codes

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

Dies erzeugt zwei Dateien:

accounts_pb2.py

enthält Klassen für die in accounts.proto definierten Messages.

accounts_pb2_grpc.py

enthält die definierten Klassen AccountsStub für den Aufruf von RPCs, AccountsServicer für die API-Definition des Services und eine Funktion add_AccountsServicer_to_server für den Server.

Server erstellen

Hierfür schreiben wir die Datei accounts_server.py:

from concurrent import futures
import logging

import grpc

import accounts_pb2_grpc as accounts_service
import accounts_pb2 as accounts_messages


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()

Client erstellen

Hierfür schreiben wir accounts_client.py:

import logging

import grpc

import accounts_pb2_grpc as accounts_service
import accounts_pb2 as accounts_messages


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()

Client und Server starten

  1. Starten des Server:

    $ pipenv run python accounts_server.py
    
  2. Starten des Client von einem anderen Terminal aus:

    $ pipenv run python accounts_client.py
    Account created: tom