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:
// 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:
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 Funktionadd_AccountsServicer_to_server
für den Server.
Server erstellen¶
Hierfür schreiben wir die Datei 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
:
# 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¶
Starten des Server:
$ uv run python accounts_server.py
Starten des Client von einem anderen Terminal aus:
$ uv run python accounts_client.py Account created: tom