Qt GRPC Kundenleitfaden
Der Kundenleitfaden für „ Qt GRPC “.
Dienstleistungsmethoden
In gRPC™einem Protobuf-Schema können Servicemethoden definiert werden, um die Kommunikation zwischen Clients und Servern festzulegen. Der Protobuf-Compiler, protoc, kann dann auf Grundlage dieser Definitionen die erforderlichen Server- und Client-Schnittstellen generieren. gRPC unterstützt vier Arten von Servicemethoden:
- Unäre Aufrufe – Der Client sendet eine einzelne Anfrage und erhält eine einzelne Antwort.
rpc UnaryCall (Request) returns (Response);Der entsprechende Client-Handler ist QGrpcCallReply.
- Server-Streaming – Der Client sendet eine einzelne Anfrage und erhält mehrere Antworten.
rpc ServerStreaming (Request) returns (stream Response);Der entsprechende Client-Handler lautet QGrpcServerStream.
- Client-Streaming – Der Client sendet mehrere Anfragen und erhält eine einzige Antwort.
rpc ClientStreaming (stream Request) returns (Response);Der entsprechende Client-Handler lautet QGrpcClientStream.
- Bidirektionales Streaming – Client und Server tauschen mehrere Nachrichten aus.
rpc BidirectionalStreaming (stream Request) returns (stream Response);Der entsprechende Client-Handler lautet QGrpcBidiStream.
gRPC Die Kommunikation beginnt immer beim Client, der den Remote Procedure Call (RPC) initiiert, indem er die erste Nachricht an den Server sendet. Der Server beendet anschließend jede Art von Kommunikation, indem er einen ` StatusCode` zurückgibt.
Alle Client-RPC-Handler leiten sich von der Klasse ` QGrpcOperation ` ab, die gemeinsame Funktionen bereitstellt. Aufgrund des asynchronen Charakters von RPCs werden diese naturgemäß über den Signals-&-Slots-Mechanismus von Qt verwaltet.
Ein wichtiges Signal, das allen RPC-Handlern gemeinsam ist, ist „ finished “, das den Abschluss eines RPC signalisiert. Der Handler sendet dieses Signal genau einmal während seiner Lebensdauer aus. Dieses Signal übergibt den entsprechenden „ QGrpcStatus “, der zusätzliche Informationen über den Erfolg oder Misserfolg des RPC liefert.
Darüber hinaus gibt es operation-spezifische Funktionen, wie beispielsweise „ messageReceived “ für eingehende Nachrichten, „ writeMessage “ für das Senden von Nachrichten an den Server und „ writesDone “ für das Beenden der clientseitigen Kommunikation. Die folgende Tabelle gibt einen Überblick über die unterstützten Funktionen der RPC-Client-Handler:
| Funktionalität | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
|---|---|---|---|---|
| finished | ✓ (read – endgültige Antwort) | ✓ | ✓ (read -Endantwort) | ✓ |
| messageReceived | ✗ | ✓ | ✗ | ✓ |
| writeMessage | ✗ | ✗ | ✓ | ✓ |
| writesDone | ✗ | ✗ | ✓ | ✓ |
Erste Schritte
Um die C++-API von „ Qt GRPC “ zu nutzen, verwenden Sie zunächst ein bereits vorhandenes Protobuf-Schema oder definieren Sie ein eigenes. Wir verwenden die Datei „ clientguide.proto “ als Beispiel:
syntax = "proto3";
package client.guide; // enclosing namespace
message Request {
int64 time = 1;
sint32 num = 2;
}
message Response {
int64 time = 1;
sint32 num = 2;
}
service ClientGuideService {
rpc UnaryCall (Request) returns (Response);
rpc ServerStreaming (Request) returns (stream Response);
rpc ClientStreaming (stream Request) returns (Response);
rpc BidirectionalStreaming (stream Request) returns (stream Response);
}Um diese .proto-Datei für unseren „ Qt GRPC “-Client in C++ zu verwenden, müssen wir den „ protoc “-Compiler mit den Qt-Generator-Plugins ausführen. Glücklicherweise stellt Qt die CMake-Funktionen „qt_add_grpc“ und „qt_add_protobuf“ zur Verfügung, um diesen Vorgang zu vereinfachen.
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")
find_package(Qt6 COMPONENTS Protobuf Grpc)
qt_standard_project_setup(REQUIRES 6.9)
qt_add_executable(clientguide_client main.cpp interceptors.cpp)
# Using the executable as input target will append the generated files to it.
qt_add_protobuf(clientguide_client
PROTO_FILES ${proto_files}
)
qt_add_grpc(clientguide_client CLIENT
PROTO_FILES ${proto_files}
)
target_link_libraries(clientguide_client PRIVATE Qt6::Protobuf Qt6::Grpc)Dadurch werden im aktuellen Build-Verzeichnis zwei Header-Dateien generiert:
- clientguide.qpb.h: Generiert von qtprotobufgen. Deklariert die Protobuf-Nachrichten „
Request“ und „Response“ aus dem Schema. - clientguide_client.grpc.qpb.h: Von qtgrpcgen generiert. Deklariert die Client-Schnittstelle zum Aufruf der Methoden eines „ gRPC “-Servers, der das „
ClientGuideService“ aus dem Schema implementiert.
Die folgende Client-Schnittstelle wird generiert:
namespace client::guide {
namespace ClientGuideService {
class Client : public QGrpcClientBase
{
...
std::unique_ptr<QGrpcCallReply> UnaryCall(const client::guide::Request &arg);
std::unique_ptr<QGrpcServerStream> ServerStreaming(const client::guide::Request &arg);
std::unique_ptr<QGrpcClientStream> ClientStreaming(const client::guide::Request &arg);
std::unique_ptr<QGrpcBidiStream> BidirectionalStreaming(const client::guide::Request &arg);
...
};
} // namespace ClientGuideService
} // namespace client::guideHinweis: Der Benutzer ist dafür verantwortlich, die von der Schnittstelle „ Client “ zurückgegebenen eindeutigen RPC-Handler zu verwalten und sicherzustellen, dass diese mindestens so lange bestehen bleiben, bis das Signal „ finished “ ausgelöst wird. Nach dem Empfang dieses Signals kann der Handler sicher neu zugewiesen oder zerstört werden.
Server-Einrichtung
Die Server-Implementierung für die Schnittstelle „ ClientGuideService “ folgt einem einfachen Ansatz. Sie überprüft das Feld „ time “ der Anforderungsnachricht und gibt den Statuscode „ INVALID_ARGUMENT “ zurück, wenn der Zeitpunkt in der Zukunft liegt:
const auto time = now();
if (request->time() > time)
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };Zudem setzt der Server in jeder Antwortnachricht die aktuelle Uhrzeit:
response->set_num(request->num());
response->set_time(time);
return grpc::Status::OK;Bei gültigen „ time “-Anfragen verhalten sich die Servicemethoden wie folgt:
UnaryCall: Antwortet mit dem Feld „num“ aus der Anfrage.ServerStreaming: Sendet „num“-Antworten, die mit der Anforderungsnachricht übereinstimmen.ClientStreaming: Zählt die Anzahl der Anforderungsnachrichten und legt diese Anzahl als „num“ fest.BidirectionalStreaming: Antwortet sofort mit dem Feld „num“ aus jeder eingehenden Anfrage.
Client-Einrichtung
Zunächst binden wir die generierten Header-Dateien ein:
#include "clientguide.qpb.h"
#include "clientguide_client.grpc.qpb.h"Für dieses Beispiel erstellen wir die Klasse „ ClientGuide “, um die gesamte Kommunikation zu verwalten und so die Nachverfolgung zu vereinfachen. Zunächst richten wir das Rückgrat der gesamten „ gRPC “-Kommunikation ein: einen Kanal.
channel = std::make_shared<QGrpcHttp2Channel>(
QUrl("http://localhost:50056")
/* without channel options. */
);
ClientGuide clientGuide(channel);Die Bibliothek „ Qt GRPC “ bietet „ QGrpcHttp2Channel “ an, das Sie über „ attach “ an die generierte Client-Schnittstelle binden können:
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
{
m_client.attachChannel(std::move(channel));
}Mit dieser Konfiguration kommuniziert der Client über HTTP/2 unter Verwendung von TCP als Transportprotokoll. Die Kommunikation erfolgt unverschlüsselt (d. h. ohne SSL/TLS-Einrichtung).
Erstellen einer Anforderungsnachricht
Hier ist ein einfacher Wrapper zum Erstellen von Anforderungsnachrichten:
static guide::Request createRequest(int32_t num,
ExpectedResult expected = ExpectedResult::Success)
{
guide::Request request;
request.setNum(num);
// The server-side logic fails the RPC if the time is in the future.
const auto time = expected == ExpectedResult::Failure ?
std::numeric_limits<int64_t>::max() : now();
request.setTime(time);
return request;
}Diese Funktion nimmt eine Ganzzahl und einen optionalen booleschen Wert entgegen. Standardmäßig verwenden ihre Nachrichten die aktuelle Uhrzeit, sodass die Serverlogik sie akzeptieren sollte. Wird die Funktion jedoch mit dem Wert „ fail “ auf „ true “ aufgerufen, erzeugt sie Nachrichten, die der Server ablehnen soll.
Einmalige RPCs
Es gibt verschiedene Paradigmen für die Arbeit mit RPC-Client-Handlern. Konkret können Sie sich für ein klassenbasiertes Design entscheiden, bei dem der RPC-Handler ein Mitglied der umschließenden Klasse ist, oder Sie können die Lebensdauer des RPC-Handlers über das Signal „ finished “ verwalten.
Bei der Anwendung des Single-Shot-Paradigmas sind zwei wichtige Punkte zu beachten. Der folgende Code veranschaulicht, wie dies bei unären Aufrufen funktioniert, gilt jedoch gleichermaßen für jeden anderen RPC-Typ.
std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(requestMessage);
const auto *replyPtr = reply.get(); // 1
QObject::connect(
replyPtr, &QGrpcCallReply::finished, replyPtr,
[reply = std::move(reply)](const QGrpcStatus &status) {
...
},
Qt::SingleShotConnection // 2
);- 1: Da wir die Lebensdauer des eindeutigen RPC-Objekts innerhalb des Lambda-Ausdrucks verwalten, würde das Verschieben in den Capture-Bereich des Lambda-Ausdrucks „
get()“ und andere Mitgliedsfunktionen ungültig machen. Daher müssen wir die Adresse des Zeigers kopieren, bevor wir ihn verschieben. - 2: Das Signal ` finished ` wird nur einmal ausgesendet, wodurch es sich um eine echte „Single-Shot“-Verbindung handelt. Es ist wichtig, diese Verbindung als „ SingleShotConnection “ zu kennzeichnen! Andernfalls wird der Capture von `
reply` nicht zerstört, was zu einem versteckten Speicherleck führt, das schwer zu entdecken ist.
Das Argument „ SingleShotConnection “ im Aufruf von „ connect “ stellt sicher, dass der Slot-Funktor (das Lambda) nach seiner Ausgabe zerstört wird, wodurch die mit dem Slot verbundenen Ressourcen, einschließlich seiner Captures, freigegeben werden.
Remote-Prozeduraufrufe
Unäre Aufrufe
Bei unären Aufrufen muss lediglich das Signal „ finished “ abgefangen werden. Wenn dieses Signal ausgelöst wird, können wir den „ status “ des RPC überprüfen, um festzustellen, ob der Aufruf erfolgreich war. War dies der Fall, können wir die einzige und abschließende Antwort vom Server mit „ read “ abrufen.
In diesem Beispiel verwenden wir das Single-Shot-Paradigma. Lesen Sie den Abschnitt „Single-Shot-RPCs“ unbedingt sorgfältig durch.
void unaryCall(const guide::Request &request, const QGrpcCallOptions&opts = { })
{
std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request, opts);
const auto *replyPtr = reply.get();
connect(
replyPtr, &QGrpcCallReply::finished, replyPtr,
[reply = std::move(reply)](const QGrpcStatus&status) {
if (status.isOk()) {
if (const auto response = reply->read<guide::Response>())
qDebug() << "Client (UnaryCall) finished, received:" << *response;
sonst
qDebug("Client (UnaryCall) deserialization failed");
} else {
qDebug() << "Client (UnaryCall) failed:" << status;
}
},
Qt::SingleShotConnection);
}Die Funktion startet den RPC, indem sie die Memberfunktion ` UnaryCall ` der generierten Client-Schnittstelle ` m_client` aufruft. Die Lebensdauer wird ausschließlich über das Signal ` finished ` verwaltet.
Ausführung des Codes
In „ main “ rufen wir diese Funktion einfach dreimal auf, wobei der zweite Aufruf fehlschlägt:
clientGuide.unaryCall(ClientGuide::createRequest(1));
clientGuide.unaryCall(ClientGuide::createRequest(2, ExpectedResult::Failure));
clientGuide.unaryCall(ClientGuide::createRequest(3));Eine mögliche Ausgabe bei der Ausführung könnte wie folgt aussehen:
Welcome to the clientguide!
Starting the server process ...
Server listening on: localhost:50056
Server (UnaryCall): Request( time: 1733498584776, num: 1 )
Server (UnaryCall): Request( time: 9223372036854775807, num: 2 )
Server (UnaryCall): Request( time: 1733498584776, num: 3 )
Client (UnaryCall) finished, received: Response( time: 1733498584778257 , num: 1 )
Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" )
Client (UnaryCall) finished, received: Response( time: 1733498584778409 , num: 3 )Wir sehen, dass der Server die drei Nachrichten empfängt, wobei die zweite einen großen Wert für die Zeit enthält. Auf der Client-Seite gaben der erste und der letzte Aufruf den Statuscode „ Ok “ zurück, während die zweite Nachricht aufgrund der in der Zukunft liegenden Nachrichtenzeit mit dem Statuscode „ InvalidArgument “ fehlschlug.
Server-Streaming
Bei einem Server-Stream sendet der Client eine erste Anfrage, und der Server antwortet mit einer oder mehreren Nachrichten. Zusätzlich zum Signal „ finished “ müssen Sie auch das Signal „ messageReceived “ verarbeiten.
In diesem Beispiel verwenden wir das Single-Shot-Paradigma, um den Lebenszyklus des Streaming-RPCs zu verwalten. Lesen Sie den Abschnitt „Single-Shot-RPCs“ sorgfältig durch.
Wie bei jedem RPC verbinden wir uns zunächst mit dem Signal „ finished “:
void serverStreaming(const guide::Request &initialRequest)
{
std::unique_ptr<QGrpcServerStream> stream = m_client.ServerStreaming(initialRequest);
const auto *streamPtr = stream.get();
connect(
streamPtr, &QGrpcServerStream::finished, streamPtr,
[stream = std::move(stream)](const QGrpcStatus&status) {
if (status.isOk())
qDebug("Client (ServerStreaming) finished");
sonst
qDebug() << "Client (ServerStreaming) failed:" << status;
},
Qt::SingleShotConnection);Um die Server-Meldungen zu verarbeiten, binden wir uns an das Signal „ messageReceived “ an und rufen „ read “ auf, sobald das Signal ausgelöst wird.
connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] {
if (const auto response = streamPtr->read<guide::Response>())
qDebug() << "Client (ServerStream) received:" << *response;
sonst
qDebug("Client (ServerStream) deserialization failed");
});
}Ausführen des Codes
Die Serverlogik sendet den in der ursprünglichen Anfrage empfangenen Wert an den Client zurück. Wir erstellen eine solche Anfrage und rufen die Funktion auf.
clientGuide.serverStreaming(ClientGuide::createRequest(3));Eine mögliche Ausgabe bei Ausführung des Server-Streaming könnte wie folgt aussehen:
Welcome to the clientguide!
Starting the server process ...
Server listening on: localhost:50056
Server (ServerStreaming): Request( time: 1733504435800, num: 3 )
Client (ServerStream) received: Response( time: 1733504435801724 , num: 0 )
Client (ServerStream) received: Response( time: 1733504435801871 , num: 1 )
Client (ServerStream) received: Response( time: 1733504435801913 , num: 2 )
Client (ServerStreaming) finishedSobald der Server startet, empfängt er eine Anfrage mit dem Wert „num“ gleich 3 und antwortet mit drei „ Response “-Meldungen, bevor die Kommunikation beendet wird.
Client-Streaming
Bei einem Client-Stream sendet der Client eine oder mehrere Anfragen, und der Server antwortet mit einer einzigen abschließenden Antwort. Das Signal „ finished “ muss verarbeitet werden, und Nachrichten können mithilfe der Funktion „ writeMessage “ gesendet werden. Mit der Funktion „ writesDone “ kann dann angegeben werden, dass der Client das Schreiben beendet hat und keine weiteren Nachrichten mehr gesendet werden.
Wir verwenden einen klassenbasierten Ansatz für die Interaktion mit dem Streaming-RPC, wobei der Handler als Mitglied der Klasse integriert wird. Wie bei jedem RPC verbinden wir uns mit dem Signal „ finished “:
void clientStreaming(const guide::Request &initialRequest)
{
m_clientStream = m_client.ClientStreaming(initialRequest);
for (int32_t i = 1; i < 3;++i)
m_clientStream->writeMessage(createRequest(initialRequest.num() + i));
m_clientStream->writesDone();
connect(m_clientStream.get(), &QGrpcClientStream::finished, m_clientStream.get(),
[this](const QGrpcStatus&status) {
if (status.isOk()) {
if (const auto response = m_clientStream->read<guide::Response>()) {
qDebug() << "Client (ClientStreaming) finished, received:"
<< *Antwort;
}
m_clientStream.reset();
} else {
qDebug() << "Client (ClientStreaming) failed:" << status;
qDebug("Restarting the client stream");
clientStreaming(createRequest(0));
}
});
}Die Funktion startet den Client-Stream mit einer ersten Nachricht. Anschließend schreibt sie zwei weitere Nachrichten, bevor sie durch den Aufruf von ` writesDone` das Ende der Kommunikation signalisiert. Wenn der Streaming-RPC erfolgreich ist, werden die abschließende Antwort vom Server mit ` read ` abgerufen und das RPC-Objekt mit ` reset ` freigegeben. Fällt der RPC fehl, führen wir einen erneuten Versuch durch, indem wir dieselbe Funktion aufrufen, die das Mitglied ` m_clientStream ` überschreibt und das Signal ` finished ` erneut verbindet. Wir können das Mitglied ` m_clientStream ` innerhalb des Lambda-Ausdrucks nicht einfach neu zuweisen, da dadurch die erforderliche Verbindung verloren ginge.
Ausführen des Codes
In „ main “ rufen wir die Funktion „ clientStreaming “ mit einer fehlerhaften Meldung auf, wodurch ein RPC-Fehler ausgelöst und die Wiederholungslogik ausgeführt wird.
clientGuide.clientStreaming(ClientGuide::createRequest(0, ExpectedResult::Failure));Eine mögliche Ausgabe bei Ausführung des Client-Streamings könnte wie folgt aussehen:
Welcome to the clientguide!
Starting the server process ...
Server listening on: localhost:50056
Server (ClientStreaming): Request( time: 9223372036854775807, num: 0 )
Client (ClientStreaming) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" )
Restarting the client stream
Server (ClientStreaming): Request( time: 1733912946696, num: 0 )
Server (ClientStreaming): Request( time: 1733912946697, num: 1 )
Server (ClientStreaming): Request( time: 1733912946697, num: 2 )
Client (ClientStreaming) finished, received: Response( time: 1733912946696922 , num: 3 )Der Server empfängt eine erste Nachricht, die einen RPC-Fehler verursacht und damit die Wiederholungslogik auslöst. Bei der Wiederholung wird der RPC mit einer gültigen Nachricht gestartet; anschließend werden drei Nachrichten an den Server gesendet, bevor der Vorgang ordnungsgemäß abgeschlossen wird.
Bidirektionales Streaming
Das bidirektionale Streaming bietet die größte Flexibilität, da sowohl der Client als auch der Server gleichzeitig Nachrichten senden und empfangen können. Es erfordert die Verarbeitung der Signale „ finished “ und „ messageReceived “ und stellt die Schreibfunktionalität über „ writeMessage “ bereit.
Zur Veranschaulichung der Funktionalität verwenden wir einen klassenbasierten Ansatz mit Slot-Verbindungen für Mitgliedsfunktionen, wobei der Handler als Mitglied der Klasse integriert wird. Zusätzlich nutzen wir die zeigerbasierte Funktion ` read `. Die beiden verwendeten Mitglieder sind:
std::unique_ptr<QGrpcBidiStream> m_bidiStream;
guide::Response m_bidiResponse;Wir erstellen eine Funktion, um das bidirektionale Streaming ausgehend von einer ersten Nachricht zu starten, und verbinden die Slot-Funktionen mit den jeweiligen Signalen „ finished “ und „ messageReceived “.
void bidirectionalStreaming(const guide::Request &initialRequest)
{
m_bidiStream = m_client.BidirectionalStreaming(initialRequest);
connect(m_bidiStream.get(), &QGrpcBidiStream::finished, this, &ClientGuide::bidiFinished);
connect(m_bidiStream.get(), &QGrpcBidiStream::messageReceived, this,
&ClientGuide::bidiMessageReceived);
}Die Funktionsweise der Slots ist unkompliziert. Der Slot „ finished “ gibt das RPC-Objekt lediglich aus und setzt es zurück:
void bidiFinished(const QGrpcStatus&status)
{
if (status.isOk())
qDebug("Client (BidirectionalStreaming) finished");
sonst
qDebug() << "Client (BidirectionalStreaming) failed:" << status;
m_bidiStream.reset();
}Der Slot „ messageReceived “ readt die Nachrichten in das Member „ m_bidiResponse “ ein und schreibt so lange Nachrichten, bis die Nummer der empfangenen Antwort Null erreicht. An diesem Punkt schließen wir die clientseitige Kommunikation mit „ writesDone “ halb.
void bidiMessageReceived()
{
if (m_bidiStream->read(&m_bidiResponse)) {
qDebug() << "Client (BidirectionalStreaming) received:" << m_bidiResponse;
if (m_bidiResponse.num() > 0) {
m_bidiStream->writeMessage(createRequest(m_bidiResponse.num() - 1));
return;
}
} else {
qDebug("Client (BidirectionalStreaming) deserialization failed");
}
m_bidiStream->writesDone();
}Ausführen des Codes
Die Serverlogik gibt einfach eine Nachricht zurück, sobald sie etwas liest, und erstellt dabei eine Antwort mit der Nummer aus der Anfrage. In ` main` erstellen wir eine solche Anfrage, die letztendlich als Zähler dient.
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));Eine mögliche Ausgabe bei Ausführung des bidirektionalen Streamings könnte wie folgt aussehen:
Welcome to the clientguide!
Starting the server process ...
Server listening on: localhost:50056
Server (BidirectionalStreaming): Request( time: 1733503832107, num: 3 )
Client (BidirectionalStreaming) received: Response( time: 1733503832108708 , num: 3 )
Server (BidirectionalStreaming): Request( time: 1733503832109, num: 2 )
Client (BidirectionalStreaming) received: Response( time: 1733503832109024 , num: 2 )
Server (BidirectionalStreaming): Request( time: 1733503832109, num: 1 )
Client (BidirectionalStreaming) received: Response( time: 1733503832109305 , num: 1 )
Server (BidirectionalStreaming): Request( time: 1733503832109, num: 0 )
Client (BidirectionalStreaming) received: Response( time: 1733503832109529 , num: 0 )
Client (BidirectionalStreaming) finishedClientGuide-Interceptoren
Eine Einführung in den Interception-Mechanismus und die Rolle von Interceptor-Schnittstellen im Lebenszyklus eines RPC finden Sie in der Übersicht zu Interceptoren unterQt GRPC .
Das Beispiel im Client-Leitfaden veranschaulicht drei Interceptoren, die in einer einzigen Kette kombiniert sind:
- Ein Metrik -Interceptor, der RPC-Statistiken aufzeichnet und eine Trace-ID einfügt.
- Ein Authentifizierungs -Interceptor, der ein Bearer-Token anhängt.
- Ein Sanitizing -Interceptor, der Nachrichten validiert und normalisiert.
Zusammen veranschaulichen sie, wie Interceptors den RPC-Datenverkehr in verschiedenen Phasen des Aufrufzyklus überwachen, modifizieren und validieren können.
Metrik-Interceptor
Der Metrik-Interceptor wird an erster Stelle in der Kette platziert, damit er den gesamten RPC-Lebenszyklus überwachen kann. Er erfasst grundlegende Statistiken wie die Größe der empfangenen Nutzdaten, die RPC-Dauer sowie die Anzahl der erfolgreichen und fehlgeschlagenen Aufrufe für jede RPC-Methode.
class MetricsInterceptor final : public QGrpcStartInterceptor,
public QGrpcMessageReceivedInterceptor,
public QGrpcFinishedInterceptor
{
public:
~MetricsInterceptor() override;
Continuation onStart(QGrpcInterceptionContext &context,
QProtobufMessage &message,
QGrpcCallOptions &callOptions) override;
void onMessageReceived(QGrpcInterceptionContext &context,
QByteArray &messageData) override;
void onFinished(QGrpcInterceptionContext &context,
QGrpcStatus &status) override;
private:
using Clock = std::chrono::steady_clock;
using Ms = std::chrono::duration<double, std::milli>;
struct RpcMetrics {
quint64 bytesReceived = 0;
quint64 completed = 0;
quint64 failed = 0;
QList<double> durations;
};
QHash<quint64, Clock::time_point> m_activeRPCs;
QMap<QtGrpc::RpcDescriptor, RpcMetrics> m_metrics;
};Implementierung des Metrik-Interceptors
Wenn ein RPC startet, erfasst der Interceptor den aktuellen Zeitstempel und speichert ihn unter Verwendung der eindeutigen „ operationId “ als Schlüssel. Außerdem wird den ausgehenden Metadaten eine Trace-ID hinzugefügt, damit der Aufruf dienstübergreifend korreliert werden kann.
QGrpcStartInterceptor::Continuation
MetricsInterceptor::onStart(QGrpcInterceptionContext &context,
QProtobufMessage & /*message*/,
QGrpcCallOptions &callOptions)
{
m_activeRPCs.insert(context.operationId(), std::chrono::steady_clock::now());
callOptions.addMetadata("x-trace-id"_L1, QUuid::createUuid().toByteArray());
return Continuation::Proceed;
}
void MetricsInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
QByteArray &messageData)
{
m_metrics[context.descriptor()].bytesReceived += messageData.size();
}
void MetricsInterceptor::onFinished(QGrpcInterceptionContext &context,
QGrpcStatus &status)
{
const auto it = m_activeRPCs.find(context.operationId());
Q_ASSERT(it != m_activeRPCs.cend());
const auto duration = Ms(Clock::now() - it.value()).count();
m_activeRPCs.erase(it);
auto &metrics = m_metrics[context.descriptor()];
if (!status.isOk()) {
++metrics.failed;
return;
}
++metrics.completed;
metrics.durations.append(duration);
}Solange der RPC aktiv ist, tragen eingehende Nachrichten zur Erfassung der Metriken bei, indem sie die Anzahl der empfangenen Nutzdaten-Bytes akkumulieren.
Wenn der RPC beendet ist, berechnet der Interceptor die Gesamtdauer und aktualisiert die aggregierten Statistiken pro RPC-Deskriptor.
Zu Demonstrationszwecken werden die erfassten Metriken ausgegeben, wenn die Interceptor-Instanz zerstört wird.
MetricsInterceptor::~MetricsInterceptor()
{
Q_ASSERT(m_activeRPCs.isEmpty());
const auto keys = m_metrics.keys();
for (const auto &k: keys) {
qInfo() << "Metrics for:" << k;
const auto &m = m_metrics[k];
qInfo() << " Bytes received :" << m.bytesReceived;
qInfo() << " Failed RPCs :" << m.failed;
qInfo() << " Completed RPCs :" << m.completed;
qInfo() << " Completed Durations (ms):" << m.durations;
}
}Authentifizierungs-Interceptor
Der Authentifizierungs-Interceptor fügt ausgehenden RPC-Aufrufen ein Bearer-Token hinzu. Wenn die Anwendung bereits einen „ authorization “-Header bereitstellt, lässt der Interceptor diesen unverändert.
Meldet der Server einen Authentifizierungsfehler, aktualisiert der Interceptor das Token, sodass nachfolgende RPC-Aufrufe die aktualisierten Anmeldedaten verwenden.
class TokenProvider
{
public:
TokenProvider();
QByteArray token();
void refresh();
private:
QByteArray m_token;
};
class AuthInterceptor final : public QGrpcStartInterceptor,
public QGrpcTrailingMetadataInterceptor,
public QGrpcFinishedInterceptor
{
public:
Continuation onStart(QGrpcInterceptionContext &context,
QProtobufMessage &message,
QGrpcCallOptions &callOptions) override;
void onTrailingMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) override;
void onFinished(QGrpcInterceptionContext &context,
QGrpcStatus &status) override;
private:
TokenProvider m_tokenProvider;
QByteArray m_serverAuthHint;
};Implementierung des Auth-Interceptors
Der „ TokenProvider “ kapselt die Token-Verwaltung. In diesem Beispiel generiert er ausschließlich zu Demonstrationszwecken eine vereinfachte Token-Zeichenkette. Diese stellt kein echtes Authentifizierungstoken dar. In realen Anwendungen würden Token in der Regel über ein externes Identitätssystem bezogen und aktualisiert, das für die Ausgabe und Verwaltung von Authentifizierungstoken zuständig ist.
TokenProvider::TokenProvider() { refresh(); }
QByteArray TokenProvider::token() { return m_token; }
void TokenProvider::refresh()
{
// NOTE: This is NOT a real JWT or secure token.
// It only mimics the structure: header.payload.signature
// The payload is just a timestamp used for expiry checks.
auto header = R"({"alg":"none","typ":"JWT"})"_ba; // fake header
auto payload = QByteArray::number(now()); // fake "iat"
auto signature = "no_signature"_ba; // fake signing
m_token = header + '.' + payload + '.' + signature;
}Auf der Serverseite werden bei eingehenden Aufrufen die Metadaten des „ authorization “ validiert. Wenn die Validierung fehlschlägt, gibt der Server den Status „ Unauthenticated “ zurück und liefert zusätzliche Informationen in den nachgestellten Metadaten.
if (auto authStatus = validateAuth(context); !authStatus.ok())
return authStatus;Zu Beginn eines RPC-Aufrufs prüft der Interceptor, ob der Aufruf bereits einen „ authorization “-Header enthält. Ist dies nicht der Fall, fügt er den ausgehenden Metadaten ein Bearer-Token hinzu.
Die nachgestellten Metadaten werden auf vom Server zurückgegebene Authentifizierungshinweise überprüft. Ist ein solcher Hinweis vorhanden, wird er gespeichert, damit er gemeldet werden kann, falls der Aufruf letztendlich mit einem Authentifizierungsfehler fehlschlägt.
Wenn der RPC mit dem Status „ Unauthenticated “ endet, aktualisiert der Interceptor das Token. In diesem Beispiel wird bei der Aktualisierung lediglich ein Token mit dem neuen aktuellen Zeitpunkt neu generiert. In realen Szenarien würde dies die Anforderung eines neuen Tokens von einem externen Identitätssystem beinhalten.
QGrpcStartInterceptor::Fortsetzung
AuthInterceptor::onStart(QGrpcInterceptionContext& /*context*/,
QProtobufMessage& /*message*/,
QGrpcCallOptions&callOptions)
{
constexpr QByteArrayView AuthKey("authorization");
if (!callOptions.metadata(QtGrpc::MultiValue).contains(AuthKey))
callOptions.addMetadata(AuthKey, „Bearer “ + m_tokenProvider.token());
return Continuation::Proceed;
}
void AuthInterceptor::onTrailingMetadata(QGrpcInterceptionContext& /*context*/,
QMultiHash<QByteArray, QByteArray> &metadata)
{
constexpr QByteArrayView AuthErrorKey("www-authenticate");
const auto it = metadata.constFind(AuthErrorKey);
if (it== metadata.cend())
return;
m_serverAuthHint = it.value();
}
void AuthInterceptor::onFinished(QGrpcInterceptionContext& /*context*/, QGrpcStatus&status)
{
// Nur Status-Codes für „Nicht authentifiziert“ behandeln.
if (status.code() != QtGrpc::StatusCode::Unauthenticated)
return;
qDebug("[AuthInterceptor] Refreshing token for next call");
// HINWEIS: Zur Veranschaulichung vereinfacht. Echte Token werden über einen ordnungsgemäßen Authentifizierungsablauf bezogen.
m_tokenProvider.refresh();
if (!m_serverAuthHint.isEmpty()) {
status = QGrpcStatus{ status.code(), status.message() + u", Hinweis: "_s + m_serverAuthHint };
m_serverAuthHint.clear();
}
}Interceptor zur Bereinigung
Der Sanitizing-Interceptor validiert und normalisiert Anfrage- und Antwortdaten. Er veranschaulicht, wie Interceptoren grundlegende clientseitige Einschränkungen durchsetzen können, bevor Nachrichten gesendet und nachdem Antworten empfangen werden.
In diesem Beispiel begrenzt der Interceptor die Werte der Anfrage auf zulässige Bereiche, normalisiert Zeitstempel und lehnt Aufrufe ab, deren Metadaten eine vordefinierte Größenbeschränkung überschreiten.
class SanitizingInterceptor final : public QGrpcStartInterceptor,
public QGrpcWriteMessageInterceptor,
public QGrpcMessageReceivedInterceptor
{
public:
Continuation onStart(QGrpcInterceptionContext &context,
QProtobufMessage &message,
QGrpcCallOptions &callOptions) override;
void onWriteMessage(QGrpcInterceptionContext &context,
QProtobufMessage &message) override;
void onMessageReceived(QGrpcInterceptionContext &context,
QByteArray &messageData) override;
};Implementierung des Sanitizing-Interceptors
Hilfsfunktionen werden verwendet, um die Gesamtgröße der Metadaten zu berechnen und grundlegende Normalisierungsregeln auf Anfrage- und Antwortnachrichten anzuwenden.
qsizetype getTotalBytes(const QMultiHash<QByteArray, QByteArray> &md)
{
qsizetype total = 0;
for (const auto &[k, v] : md.asKeyValueRange())
total += k.size() + v.size();
return total;
}
template <typename T>
void sanitizeMessage(T &request)
{
const auto num = request.num();
if (num < 0 || num > 5)
request.setNum(qBound(0, num, 5));
const auto time = request.time();
const auto currentTime = now();
if (time < 0 || time > currentTime)
request.setTime(qMax(0, currentTime));
}Zu Beginn eines RPC-Aufrufs überprüft der Interceptor zunächst, ob die ausgehenden Metadaten die zulässige Größe nicht überschreiten. Wird das Limit überschritten, wird der Aufruf durch Rückgabe von Continuation::Drop
Stimmt die Anforderungsnachricht mit dem erwarteten Typ überein, werden ihre Felder vor dem Versand des RPC bereinigt.
Bei Streaming-Aufrufen werden ausgehende Nachrichten zusätzlich im Hook „ onWriteMessage “ bereinigt.
Schließlich werden eingehende Antworten abgefangen. Die serialisierte Nutzlast wird deserialisiert, normalisiert und erneut serialisiert, bevor sie an die Anwendung weitergeleitet wird.
QGrpcStartInterceptor::Fortsetzung
SanitizingInterceptor::onStart(QGrpcInterceptionContext&context, QProtobufMessage&message,
QGrpcCallOptions&callOptions)
{
constexpr quint16 MaxMetadata = 1024 * 8; // 8 KiB
auto totalMdBytes = getTotalBytes(callOptions.metadata(QtGrpc::MultiValue));
if (totalMdBytes > MaxMetadata) {
qDebug() << "[Sanitizing] Aborting call with metadata size of" << totalMdBytes << "bytes";
return Continuation::Drop;
}
onWriteMessage(context, message);
return Continuation::Proceed;
}
void SanitizingInterceptor::onWriteMessage(QGrpcInterceptionContext& /*context*/,
QProtobufMessage&message)
{
// Zur Behandlung komplexerer Szenarien mit verschiedenen „Request“-Nachrichtentypen
// wäre es sinnvoll , entsprechend nach context.descriptor() zu verzweigen.
if (auto *request = qprotobufmessage_cast<client::guide::Request *>(&message))
sanitizeMessage(*request);
}
void SanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext&context,
QByteArray&messageData)
{
// messageData kann nur vom Typ client::guide::Response sein. Für eine
// komplexere Verarbeitung fügen Sie eine entsprechende Verarbeitung mit context.descriptor() hinzu.
// Beachten Sie, dass die Deserialisierung und Serialisierung einen Mehraufwand verursachen.
client::guide::Response response;
auto serializer = context.channel().serializer();
if (serializer->deserialize(&response, messageData))
return;
sanitizeMessage(response);
messageData = serializer->serialize(&response);
}Verwendung von Interceptoren
Erstellen Sie die Interceptor-Kette in einer Hilfsfunktion und geben Sie sie an den Aufrufer zurück.
inline QGrpcInterceptorChain createInterceptors()
{
QGrpcInterceptorChain chain;
chain.set(
std::make_unique<MetricsInterceptor>(),
std::make_unique<AuthInterceptor>(),
std::make_unique<SanitizingInterceptor>()
);
return chain;
}Bevor Sie die Kette an den Kanal anhängen, überprüfen Sie, ob sie erfolgreich gefüllt wurde (beispielsweise durch Abfrage von ` isEmpty()`), und wenden Sie eine geeignete Fehlerbehandlungsstrategie an, falls die Erstellung fehlgeschlagen ist.
auto interceptors = createInterceptors();
if (interceptors.isEmpty()) {
qWarning("Failed to create the interceptor chain");
return EXIT_FAILURE; // oder eine andere geeignete Ausweichlösung
}
channel = std::make_shared<QGrpcHttp2Channel>(
QUrl("http://localhost:50056"),
std::move(interceptors)
);Bei unären Aufrufen plant das Beispiel eine Abfolge von Anfragen mit einer kurzen Verzögerung dazwischen ein. Dadurch lässt sich leichter beobachten, wie sich die Interceptoren auf aufeinanderfolgende Aufrufe auswirken: Trace-Metadaten werden hinzugefügt, bei Authentifizierungsfehlern wird das Token für spätere Anfragen aktualisiert, und ungültige Metadaten können dazu führen, dass ein Aufruf verworfen wird, bevor er gesendet wird.
int delayMs = 150;
for (int i = 1; i < 6; ++i) {
QTimer::singleShot(delayMs, [&, i] {
auto expected = i % 2 == 0 ? ExpectedResult::Failure : ExpectedResult::Success;
clientGuide.unaryCall(ClientGuide::createRequest(i, expected));
});
delayMs += 150;
}
QTimer::singleShot(delayMs, [&] {
QGrpcCallOptions invalidOpts;
invalidOpts.addMetadata("huge_key"_ba, "huge_value"_ba.repeated(8'000));
clientGuide.unaryCall(guide::Request{ }, invalidOpts); // this call will fail.
});
delayMs += 150;Das Beispiel führt außerdem einen bidirektionalen Streaming-RPC durch. Die „ bidirectionalStreaming() “-Implementierung würde normalerweise „ 666 “-Nachrichten austauschen, doch der Sanitizing-Interceptor begrenzt die Anforderungswerte so, dass der Stream nach fünf Austauschvorgängen beendet wird.
QTimer::singleShot(delayMs, [&] {
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(666));
});Ausführen des Codes
Führen Sie „ clientguide_client “ mit der Option „ -I “ aus, um die Interceptor-Unterstützung zu aktivieren. Eine mögliche Ausgabe ist unten dargestellt:
Welcome to the clientguide!
Running with Interceptor support
Starting the server process ...
Server listening on: localhost:50056
Server (UnaryCall): Request( time: 1774360832277, num: 1 ), Metadata: { x-trace-id: {360bb041-9ab5-4940-95c1-0d4c89491570} }
Client (UnaryCall) finished, received: Response( time: 1774360832278, num: 1 )
Server (UnaryCall): Request( time: 1774360832427, num: 2 ), Metadata: { x-trace-id: {01817a8f-9f0e-404e-a21c-767f81e74a2d} }
Client (UnaryCall) finished, received: Response( time: 1774360832429, num: 2 )
Server (UnaryCall): Request( time: 1774360832577, num: 3 ), Metadata: { x-trace-id: {78cb336f-0396-4bbc-b1ee-59914651feef} }
Client (UnaryCall) finished, received: Response( time: 1774360832578, num: 3 )
[AuthInterceptor] Refreshing token for next call
Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Unauthenticated, message: "Token expired with age: 603 ms, Hint: Bearer realm=\"clientguide\", error=\"invalid_token\"" )
Server (UnaryCall): Request( time: 1774360832876, num: 5 ), Metadata: { x-trace-id: {63a40327-2f46-43c4-9e2c-44b67f297bd2} }
Client (UnaryCall) finished, received: Response( time: 1774360832877, num: 5 )
[Sanitizing] Aborting call with metadata size of 80129 bytes
Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Aborted, message: "Interceptors dropped the call" )
Server (BidirectionalStreaming) accepted call, Metadata: { x-trace-id: {fc945bdb-5c8a-4c65-8bfb-8655800d5774} }
Server (BidirectionalStreaming): Request( time: 1774360833177, num: 5 )
Client (BidirectionalStreaming) received: Response( time: 1774360833178, num: 5 )
Server (BidirectionalStreaming): Request( time: 1774360833178, num: 4 )
Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 4 )
Server (BidirectionalStreaming): Request( time: 1774360833179, num: 3 )
Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 3 )
Server (BidirectionalStreaming): Request( time: 1774360833179, num: 2 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 2 )
Server (BidirectionalStreaming): Request( time: 1774360833180, num: 1 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 1 )
Server (BidirectionalStreaming): Request( time: 1774360833180, num: 0 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 0 )
Client (BidirectionalStreaming) finished
All operations completed! Automatically shutting down...
Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "BidirectionalStreaming", type: QtGrpc::RpcType::BidiStreaming )
Bytes received : 52
Failed RPCs : 0
Completed RPCs : 1
Completed Durations (ms): QList(3.97233)
Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "UnaryCall", type: QtGrpc::RpcType::UnaryCall )
Bytes received : 36
Failed RPCs : 2
Completed RPCs : 4
Completed Durations (ms): QList(1.937, 1.90971, 1.624, 1.50429)Die Ausgabe zeigt die Interceptor-Kette in Aktion. Ausgehende unäre Aufrufe erreichen den Server mit Trace-Metadaten, die vom Metrik-Interceptor hinzugefügt wurden. Wenn der Server einen Authentifizierungsfehler meldet, aktualisiert der Authentifizierungs-Interceptor das Token für nachfolgende Aufrufe und ergänzt die Fehlermeldung um den vom Server bereitgestellten Hinweis. Ein späterer Aufruf wird lokal vom Sanitizing-Interceptor abgelehnt, da seine Metadaten das konfigurierte Limit überschreiten. Der bidirektionale Streaming-Aufruf zeigt, dass Interceptoren auch auf Streaming-RPCs angewendet werden: Der Sanitizing-Interceptor begrenzt den Wert der Anfrage, sodass der Stream nach fünf Nachrichtenaustauschen beendet wird, anstatt von 666 fortgesetzt zu werden. Wenn der Client schließlich heruntergefahren wird, gibt der Metrics-Interceptor die gesammelten Statistiken für jede RPC-Methode aus.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.