gRPC-Beispiel

Standardmäßig verwendet gRPC Protocol Buffers (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:

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

syntax = "proto3";

message Account {

Warnung

Beachtet, 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:

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);
}

Generieren des gRPC-Codes

$ uv add grpcio grpcio-tools
$ uv 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:

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

Client erstellen

Hierfür schreiben wir 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()

Client und Server starten

  1. Starten des Server:

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

    $ uv run python accounts_client.py
    Account created: tom