Der Qt GRPC Client-Leitfaden.

Dienst-Methoden

In gRPCkönnen Service-Methoden in einem Protobuf-Schema definiert werden, um die Kommunikation zwischen Clients und Servern zu spezifizieren. Der Protobuf-Compiler protoc kann dann auf der Grundlage dieser Definitionen die erforderlichen Server- und Client-Schnittstellen generieren. gRPC unterstützt vier Arten von Service-Methoden:

  • Unary Calls - Der Client sendet eine einzige Anfrage und erhält eine einzige Antwort.
    rpc UnaryCall (Request) returns (Response);

    Der entsprechende Client-Handler ist QGrpcCallReply.

  • Server Streaming - Der Client sendet eine einzige Anfrage und erhält mehrere Antworten.
    rpc ServerStreaming (Request) returns (stream Response);

    Der entsprechende Client-Handler ist QGrpcServerStream.

  • Client Streaming - Der Client sendet mehrere Anfragen und empfängt eine einzige Antwort.
    rpc ClientStreaming (stream Request) returns (Response);

    Der entsprechende Client-Handler ist QGrpcClientStream.

  • Bidirektionales Streaming - Der Client und der Server tauschen mehrere Nachrichten aus.
    rpc BidirectionalStreaming (stream Request) returns (stream Response);

    Der entsprechende Client-Handler ist QGrpcBidiStream.

gRPC Die Kommunikation beginnt immer mit dem Client, der den Remote Procedure Call (RPC) initiiert, indem er die erste Nachricht an den Server sendet. Der Server schließt dann jede Art von Kommunikation ab, indem er eine StatusCode zurückgibt.

Alle Client-RPC-Handler sind von der Klasse QGrpcOperation abgeleitet, die gemeinsame Funktionen bereitstellt. Aufgrund des asynchronen Charakters von RPCs werden diese natürlich über den Signals & Slots Mechanismus von Qt verwaltet.

Ein Schlüsselsignal, das allen RPC-Handlern gemeinsam ist, ist finished, das den Abschluss eines RPCs anzeigt. Der Handler gibt dieses Signal genau einmal während seiner Lebensdauer aus. Dieses Signal liefert die entsprechende QGrpcStatus, die zusätzliche Informationen über den Erfolg oder Misserfolg des RPCs liefert.

Es gibt auch betriebsspezifische Funktionalitäten, wie 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:

FunktionsweiseQGrpcCallReplyQGrpcServerStreamQGrpcClientStreamQGrpcBidiStream
finished✓ (read endgültige Antwort)✓ (read endgültige Antwort)
messageReceived
writeMessage
writesDone

Erste Schritte

Um die Qt GRPC C++ API zu verwenden, verwenden Sie zunächst ein bereits verfügbares Protobuf-Schema oder definieren Sie Ihr eigenes. Wir werden die Datei clientguide.proto als Beispiel verwenden:

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 darauf ausführen. Glücklicherweise stellt Qt die CMake-Funktionen qt_add_grpc und qt_add_protobuf zur Verfügung, um diesen Prozess zu vereinfachen.

set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")

find_package(Qt6 COMPONENTS Protobuf Grpc)
qt_standard_project_setup(REQUIRES 6.8)

qt_add_executable(clientguide_client main.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)

Dies führt dazu, dass zwei Header-Dateien im aktuellen Build-Verzeichnis erzeugt werden:

  • clientguide.qpb.h: Erzeugt von qtprotobufgen. Deklariert die Request und Response protobuf-Nachrichten aus dem Schema.
  • clientguide_client.grpc.qpb.h: Erzeugt von qtgrpcgen. Deklariert die Client-Schnittstelle für den Aufruf der Methoden eines gRPC Servers, der die 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::guide

Hinweis: Der Benutzer ist für die Verwaltung der eindeutigen RPC-Handler verantwortlich, die von der Schnittstelle Client zurückgegeben werden, und muss sicherstellen, dass sie mindestens so lange existieren, bis das Signal finished ausgegeben wird. Nach Erhalt dieses Signals kann der Handler sicher neu zugewiesen oder zerstört werden.

Server-Einrichtung

Die Serverimplementierung für 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!" };

Außerdem setzt der Server in jeder Antwortnachricht die aktuelle Zeit:

response->set_num(request->num());
response->set_time(time);
return grpc::Status::OK;

Bei gültigen time Anfragen verhalten sich die Dienstmethoden wie folgt:

  • UnaryCall: Antwortet mit dem num Feld aus der Anfrage.
  • ServerStreaming: Sendet num Antworten, die mit der Anforderungsnachricht übereinstimmen.
  • ClientStreaming: Zählt die Anzahl der Anforderungsnachrichten und setzt diese Anzahl als num.
  • BidirectionalStreaming: Antwortet sofort mit dem Feld num von jeder eingehenden Anforderungsnachricht.
Client-Einrichtung

Wir beginnen mit der Einbindung der generierten Header-Dateien:

#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, damit es leichter zu verstehen ist. Wir beginnen mit der Einrichtung des Grundgerüsts der gesamten gRPC Kommunikation: einem Kanal.

auto channel = std::make_shared<QGrpcHttp2Channel>(
    QUrl("http://localhost:50056")
    /* without channel options. */
);
ClientGuide clientGuide(channel);

Die Bibliothek Qt GRPC bietet QGrpcHttp2Channel an, die Sie unter attach in die generierte Client-Schnittstelle einbinden können:

explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
{
    m_client.attachChannel(std::move(channel));
}

Bei dieser Einstellung kommuniziert der Client über HTTP/2 mit TCP als Transportprotokoll. Die Kommunikation erfolgt unverschlüsselt (d. h. ohne SSL/TLS-Einrichtung).

Erstellen einer Anforderungsnachricht

Hier ist ein einfacher Wrapper, um Anforderungsnachrichten zu erstellen:

static guide::Request createRequest(int32_t num, bool fail = false)
{
    guide::Request request;
    request.setNum(num);
    // The server-side logic fails the RPC if the time is in the future.
    request.setTime(fail ? std::numeric_limits<int64_t>::max()
                         : QDateTime::currentMSecsSinceEpoch());
    return request;
}

Diese Funktion nimmt eine ganze Zahl und optional einen booleschen Wert an. Standardmäßig verwenden ihre Nachrichten die aktuelle Zeit, so dass die Serverlogik sie akzeptieren sollte. Wenn fail jedoch auf true gesetzt ist, erzeugt sie Nachrichten, die der Server ablehnen soll.

Single Shot RPCs

Es gibt verschiedene Paradigmen für die Arbeit mit RPC-Client-Handlern. Sie können ein klassenbasiertes Design wählen, 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 gibt es zwei wichtige Dinge zu beachten. Der folgende Code zeigt, wie es bei unären Aufrufen funktionieren würde, aber es ist dasselbe 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 Lambdas verwalten, würde das Verschieben des Objekts in die Erfassung des Lambdas 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 ausgegeben, so dass es sich um eine echte Single-Shot-Verbindung handelt. Es ist wichtig, diese Verbindung als SingleShotConnection zu kennzeichnen! Andernfalls wird die Erfassung von reply nicht zerstört, was zu einem versteckten Speicherleck führt, das schwer zu entdecken ist.

Das Argument SingleShotConnection im Aufruf connect stellt sicher, dass der Slot-Faktor (das Lambda) nach der Ausgabe zerstört wird, wodurch die mit dem Slot verbundenen Ressourcen, einschließlich der Captures, freigegeben werden.

Ferngesteuerte Prozeduraufrufe

Unary-Aufrufe

Bei unären Aufrufen muss nur das Signal finished verarbeitet werden. Wenn dieses Signal ausgesendet wird, können wir die status der RPC überprüfen, um festzustellen, ob sie erfolgreich war. Wenn ja, können wir read die einzige und endgültige Antwort des Servers abrufen.

In diesem Beispiel verwenden wir das Single-Shot-Paradigma. Lesen Sie den Abschnitt über Single-Shot-RPCs sorgfältig durch.

void unaryCall(const guide::Request &request) { std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request); const auto *replyPtr = reply.get();    QObject::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");
            } sonst {                qDebug() << "Client (UnaryCall) failed:" << status;
           }},  Qt::SingleShotConnection); }

Die Funktion startet die RPC, indem sie die Funktion UnaryCall der generierten Client-Schnittstelle m_client aufruft. Die Lebensdauer wird ausschließlich durch das finished Signal verwaltet.

Ausführen 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, true)); // fail the RPC
clientGuide.unaryCall(ClientGuide::createRequest(3));

Eine mögliche Ausgabe 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 seine Zeit enthält. Auf der Client-Seite haben der erste und der letzte Aufruf den Statuscode Ok zurückgegeben, aber die zweite Nachricht schlug mit dem Statuscode InvalidArgument fehl, da der Zeitpunkt der Nachricht in der Zukunft liegt.

Server-Streaming

Bei einem Server-Stream sendet der Client eine erste Anfrage, auf die der Server mit einer oder mehreren Nachrichten antwortet. Zusätzlich zum Signal finished müssen Sie auch das Signal messageReceived verarbeiten.

In diesem Beispiel verwenden wir das Single-Shot-Paradigma, um den Streaming-RPC-Lebenszyklus zu verwalten. Lesen Sie den Abschnitt über Single-Shot-RPCs sorgfältig durch.

Wie bei jeder RPC wird zuerst eine Verbindung zum Signal finished hergestellt:

void serverStreaming(const guide::Request &initialRequest) { std::unique_ptr<QGrpcServerStream> stream = m_client.ServerStreaming(initialRequest); const auto *streamPtr = stream.get();    QObject::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-Nachrichten zu verarbeiten, stellen wir eine Verbindung zu dem Signal messageReceived her und read die Antwort, wenn das Signal ausgegeben wird.

QObject::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 ersten Anfrage erhaltenen Betrag an den Client zurück. Wir erstellen eine solche Anforderung und rufen die Funktion auf.

clientGuide.serverStreaming(ClientGuide::createRequest(3));

Eine mögliche Ausgabe des Server-Streamings 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) finished

Sobald der Server gestartet ist, empfängt er eine Anfrage mit einer Zahl von 3 und antwortet mit drei Response Nachrichten, bevor er die Kommunikation abschließt.

Client-Streaming

Bei einem Client-Streaming sendet der Client eine oder mehrere Anfragen, und der Server antwortet mit einer einzigen endgültigen Antwort. Das Signal finished muss verarbeitet werden, und die Nachrichten können mit der Funktion writeMessage gesendet werden. Die Funktion writesDone kann dann verwendet werden, um anzuzeigen, dass der Client mit dem Schreiben fertig ist und keine weiteren Nachrichten mehr gesendet werden.

Wir verwenden einen klassenbasierten Ansatz für die Interaktion mit der Streaming-RPC, indem wir den Handler als Mitglied der Klasse einbinden. Wie bei jeder RPC stellen wir eine Verbindung mit dem Signal finished her:

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();    QObject::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:"
                                         << *Response; 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. Dann schreibt sie zwei weitere Nachrichten, bevor sie das Ende der Kommunikation durch den Aufruf von writesDone signalisiert. Wenn der RPC-Stream erfolgreich ist, wird die endgültige Antwort des Servers an read und das RPC-Objekt an reset gesendet. Wenn die RPC fehlschlägt, versuchen wir es erneut, indem wir dieselbe Funktion aufrufen, die das Mitglied m_clientStream überschreibt und das Signal finished erneut aufruft. Wir können das Mitglied m_clientStream nicht einfach innerhalb des Lambdas neu zuweisen, da dadurch die notwendige Verbindung verloren gehen würde.

Ausführen des Codes

In main rufen wir die Funktion clientStreaming mit einer fehlgeschlagenen Nachricht auf, wodurch ein RPC-Fehler ausgelöst und die Wiederholungslogik ausgeführt wird.

clientGuide.clientStreaming(ClientGuide::createRequest(0, true)); // fail the RPC

Ein mögliches Ergebnis der 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 den RPC fehlschlagen lässt und die Wiederholungslogik auslöst. Die Wiederholung startet die RPC mit einer gültigen Nachricht, woraufhin drei Nachrichten an den Server gesendet werden, bevor die RPC ordnungsgemäß beendet wird.

Bidirektionales Streaming

Bidirektionales Streaming bietet die größte Flexibilität, da es sowohl dem Client als auch dem Server erlaubt, gleichzeitig Nachrichten zu senden und zu empfangen. Es erfordert die Verarbeitung der Signale finished und messageReceived und bietet die Schreibfunktionalität über writeMessage.

Wir verwenden einen klassenbasierten Ansatz mit Member-Function-Slot-Verbindungen, um die Funktionalität zu demonstrieren, indem wir den Handler als Mitglied der Klasse einbinden. Außerdem verwenden 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 von einer anfänglichen Nachricht zu starten, und verbinden die Slot-Funktionen mit den entsprechenden 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 Slot-Funktionalität ist einfach. Der Slot finished druckt einfach das RPC-Objekt 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 read s wird in das Mitglied m_bidiResponse eingefügt und schreibt so lange Nachrichten, bis die Anzahl der empfangenen Antworten Null erreicht. An diesem Punkt wird die clientseitige Kommunikation mit writesDone halb geschlossen.

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 eine Antwort mit der Nummer aus der Anfrage. In main erstellen wir eine solche Anfrage, die letztlich als Zähler dient.

clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));

Eine mögliche Ausgabe bei der Ausführung des bidirektionalen Streaming könnte so 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) finished

Beispielprojekt @ code.qt.io

© 2025 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.