Sur cette page

Qt GRPC Guide du client

Le guide du client Qt GRPC.

Méthodes de service

En gRPCil est possible de définir des méthodes de service dans un schéma protobuf pour spécifier la communication entre les clients et les serveurs. Le compilateur protobuf, protoc, peut alors générer les interfaces serveur et client nécessaires sur la base de ces définitions. gRPC prend en charge quatre types de méthodes de service :

  • Appels unaires - Le client envoie une seule demande et reçoit une seule réponse.
    rpc UnaryCall (Request) returns (Response);

    Le gestionnaire client correspondant est QGrpcCallReply.

  • Streaming serveur - Le client envoie une seule requête et reçoit plusieurs réponses.
    rpc ServerStreaming (Request) returns (stream Response);

    Le gestionnaire client correspondant est QGrpcServerStream.

  • Streaming client - Le client envoie plusieurs requêtes et reçoit une seule réponse.
    rpc ClientStreaming (stream Request) returns (Response);

    Le gestionnaire de client correspondant est QGrpcClientStream.

  • Streaming bidirectionnel - Le client et le serveur échangent plusieurs messages.
    rpc BidirectionalStreaming (stream Request) returns (stream Response);

    Le gestionnaire client correspondant est QGrpcBidiStream.

gRPC La communication commence toujours par le client, qui lance l'appel de procédure à distance (RPC) en envoyant le premier message au serveur. Le serveur conclut ensuite tout type de communication en renvoyant un message StatusCode.

Tous les gestionnaires RPC du client sont dérivés de la classe QGrpcOperation, qui fournit des fonctionnalités partagées. En raison de la nature asynchrone des RPC, elles sont naturellement gérées par le mécanisme Signals & Slots de Qt.

Un signal clé commun à tous les gestionnaires RPC est finished, qui indique la fin d'une RPC. Le gestionnaire émet ce signal exactement une fois au cours de sa vie. Ce signal délivre le QGrpcStatus correspondant, qui fournit des informations supplémentaires sur le succès ou l'échec de la RPC.

Il existe également des fonctionnalités spécifiques aux opérations, telles que messageReceived pour les messages entrants, writeMessage pour l'envoi de messages au serveur et writesDone pour la clôture de la communication côté client. Le tableau ci-dessous présente les fonctionnalités prises en charge par les gestionnaires de clients RPC :

FonctionnalitéQGrpcCallReplyQGrpcServerStreamQGrpcClientStreamQGrpcBidiStream
finished✓ (read réponse finale)✓ (read réponse finale)
messageReceived
writeMessage
writesDone

Démarrage

Pour utiliser l'API Qt GRPC C++, commencez par utiliser un schéma de protobuf déjà disponible ou définissez votre propre schéma. Nous utiliserons le fichier clientguide.proto comme exemple :

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

Pour utiliser ce fichier .proto pour notre client Qt GRPC en C++, nous devons exécuter le compilateur protoc avec les plugins du générateur Qt. Heureusement, Qt fournit les fonctions CMake qt_add_grpc et qt_add_protobuf pour rationaliser ce processus.

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)

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

Il en résulte que deux fichiers d'en-tête sont générés dans le répertoire de construction actuel :

  • clientguide.qpb.h : Généré par qtprotobufgen. Déclare les messages protobuf Request et Response du schéma.
  • clientguide_client.grpc.qpb.h: Généré par qtgrpcgen. Déclare l'interface client pour appeler les méthodes d'un serveur gRPC implémentant le ClientGuideService du schéma.

L'interface client suivante est générée :

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

Remarque : les utilisateurs sont responsables de la gestion des gestionnaires RPC uniques renvoyés par l'interface Client, en veillant à ce qu'ils existent au moins jusqu'à ce que le signal finished soit émis. Après réception de ce signal, le gestionnaire peut être réaffecté ou détruit en toute sécurité.

Configuration du serveur

L'implémentation du serveur pour ClientGuideService suit une approche simple. Il valide le champ time du message de demande, renvoyant le code d'état INVALID_ARGUMENT si l'heure se situe dans le futur :

const auto time = now();
if (request->time() > time)
    return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };

En outre, le serveur indique l'heure actuelle dans chaque message de réponse :

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

Pour les demandes time valides, les méthodes de service se comportent comme suit :

  • UnaryCall: répond avec le champ num de la demande.
  • ServerStreaming: Envoie des réponses num correspondant au message de la demande.
  • ClientStreaming: Compte le nombre de messages de demande et définit ce compte comme num.
  • BidirectionalStreaming: Répond immédiatement avec le champ num de chaque message de demande entrant.
Configuration du client

Nous commençons par inclure les fichiers d'en-tête générés :

#include "clientguide.qpb.h"
#include "clientguide_client.grpc.qpb.h"

Pour cet exemple, nous créons la classe ClientGuide pour gérer toutes les communications, ce qui facilite la suite. Nous commençons par mettre en place l'épine dorsale de toute la communication gRPC: un canal.

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

La bibliothèque Qt GRPC propose QGrpcHttp2Channel, que vous pouvez ajouter à l'interface client générée à l'aide de attach:

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

Avec cette configuration, le client communiquera sur HTTP/2 en utilisant TCP comme protocole de transport. La communication ne sera pas cryptée (c'est-à-dire sans configuration SSL/TLS).

Création d'un message de requête

Voici un simple wrapper pour créer des messages de requête :

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

Cette fonction prend un entier et un booléen optionnel. Par défaut, ses messages utilisent l'heure actuelle, de sorte que la logique du serveur devrait les accepter. Cependant, lorsqu'elle est appelée avec fail à true, elle produit des messages que le serveur doit rejeter.

RPC à un coup

Il existe différents paradigmes pour travailler avec des gestionnaires de clients RPC. En particulier, vous pouvez choisir une conception basée sur les classes où le gestionnaire RPC est un membre de la classe englobante, ou vous pouvez gérer la durée de vie du gestionnaire RPC par le biais du signal finished.

Il y a deux choses importantes à retenir lors de l'application du paradigme "single-shot". Le code ci-dessous montre comment cela fonctionne pour les appels unaires, mais c'est la même chose pour tout autre type de RPC.

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: Puisque nous gérons la durée de vie de l'objet RPC unique dans la lambda, le déplacer dans la capture de la lambda invaliderait get() et d'autres fonctions membres. Par conséquent, nous devons copier l'adresse du pointeur avant de le déplacer.
  • 2: Le signal finished n'est émis qu'une seule fois, ce qui en fait une véritable connexion unique. Il est important de marquer cette connexion comme SingleShotConnection! Sinon, la capture de reply ne sera pas détruite, conduisant à une fuite de mémoire cachée qui est difficile à découvrir.

L'argument SingleShotConnection dans l'appel connect garantit que le foncteur de slot (le lambda) est détruit après avoir été émis, libérant ainsi les ressources associées au slot, y compris ses captures.

Appels de procédures à distance

Appels unaires

Les appels unaires ne nécessitent que la gestion du signal finished. Lorsque ce signal est émis, nous pouvons vérifier le site status de la RPC pour déterminer si elle a réussi. Si c'est le cas, nous pouvons read la réponse unique et finale du serveur.

Dans cet exemple, nous utilisons le paradigme "single-shot". Veillez à lire attentivement la section sur les RPC uniques.

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;
               autre                    qDebug("Client (UnaryCall) deserialization failed");
            } else {                qDebug() << "Client (UnaryCall) failed:" << status;
            }},  Qt::SingleShotConnection) ; }

La fonction démarre la RPC en invoquant la fonction membre UnaryCall de l'interface client générée m_client. La durée de vie est uniquement gérée par le signal finished.

Exécution du code

Dans main, nous invoquons simplement cette fonction trois fois, en laissant la deuxième invocation échouer :

clientGuide.unaryCall(ClientGuide::createRequest(1));
clientGuide.unaryCall(ClientGuide::createRequest(2, true)); // fail the RPC
clientGuide.unaryCall(ClientGuide::createRequest(3));

Un résultat possible de l'exécution de cette fonction pourrait ressembler à ce qui suit :

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  )

Le serveur reçoit les trois messages, le deuxième contenant une valeur élevée pour son temps. Du côté du client, les premier et dernier appels ont renvoyé un code d'état Ok, mais le deuxième message a échoué avec le code d'état InvalidArgument parce que l'heure du message se situe dans le futur.

Streaming du serveur

Dans un flux de serveur, le client envoie une requête initiale et le serveur répond par un ou plusieurs messages. Outre le signal finished, vous devez également gérer le signal messageReceived.

Dans cet exemple, nous utilisons le paradigme "single-shot" pour gérer le cycle de vie du streaming RPC. Veillez à lire attentivement la section sur les RPC en flux unique.

Comme pour toute RPC, nous nous connectons d'abord au signal finished:

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");
           autre                qDebug() << "Client (ServerStreaming) failed:" << status;
       },  Qt::SingleShotConnection) ;

Pour gérer les messages du serveur, nous nous connectons au signal messageReceived et nous envoyons la réponse à read lorsque le signal est émis.

    QObject::connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] { if(const auto response =  streamPtr->read<guide::Response>())            qDebug() << "Client (ServerStream) received:" << *response;
       autre            qDebug("Client (ServerStream) deserialization failed");
    }) ; }
Exécution du code

La logique du serveur renvoie au client le montant reçu dans la demande initiale. Nous créons une telle demande et invoquons la fonction.

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

Un résultat possible de l'exécution de la logique serveur pourrait ressembler à ceci :

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

Une fois que le serveur a démarré, il reçoit une demande avec une valeur num de 3 et répond par trois messages Response avant de terminer la communication.

Streaming client

Dans un flux client, le client envoie une ou plusieurs demandes et le serveur répond par une seule réponse finale. Le signal finished doit être traité et les messages peuvent être envoyés à l'aide de la fonction writeMessage. La fonction writesDone peut ensuite être utilisée pour indiquer que le client a fini d'écrire et que plus aucun message ne sera envoyé.

Nous utilisons une approche basée sur les classes pour interagir avec le RPC en continu, en incorporant le gestionnaire en tant que membre de la classe. Comme pour tout RPC, nous nous connectons au 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() ; 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)) ; } }) ; }

La fonction démarre le flux client avec un message initial. Elle continue ensuite à écrire deux messages supplémentaires avant de signaler la fin de la communication en appelant writesDone. Si le flux RPC réussit, nous read la réponse finale du serveur et reset l'objet RPC. Si la RPC échoue, nous réessayons en invoquant la même fonction, qui écrase le membre m_clientStream et reconnecte le signal finished. Nous ne pouvons pas simplement réaffecter le membre m_clientStream au sein de la lambda, car cela entraînerait la perte de la connexion nécessaire.

Exécution du code

Dans main, nous invoquons la fonction clientStreaming avec un message d'échec, ce qui déclenche un échec RPC et exécute la logique de réessai.

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

Un résultat possible de l'exécution du streaming client pourrait ressembler à ceci :

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  )

Le serveur reçoit un message initial qui provoque l'échec de la RPC et déclenche la logique de réessai. La relance démarre la RPC avec un message valide, après quoi trois messages sont envoyés au serveur avant de s'achever de manière élégante.

Flux bidirectionnel

La diffusion en continu bidirectionnelle offre la plus grande souplesse, car elle permet au client et au serveur d'envoyer et de recevoir des messages simultanément. Il nécessite la gestion des signaux finished et messageReceived et fournit la fonctionnalité d'écriture par l'intermédiaire de writeMessage.

Nous utilisons une approche basée sur la classe avec des connexions de fentes de fonctions membres pour démontrer la fonctionnalité, en incorporant le gestionnaire en tant que membre de la classe. En outre, nous utilisons la fonction read basée sur un pointeur. Les deux membres utilisés sont :

std::unique_ptr<QGrpcBidiStream> m_bidiStream;
guide::Response m_bidiResponse;

Nous créons une fonction pour démarrer le flux bidirectionnel à partir d'un message initial et nous connectons les fonctions slot aux signaux finished et messageReceived respectifs.

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

La fonctionnalité du slot est simple. Le slot finished imprime et réinitialise simplement l'objet RPC :

void bidiFinished(const QGrpcStatus &status) { if (status.isOk())        qDebug("Client (BidirectionalStreaming) finished");
   autre        qDebug() << "Client (BidirectionalStreaming) failed:" << status;
    m_bidiStream.reset() ; }

L'emplacement messageReceived read s dans le membre m_bidiResponse, continuant à écrire des messages jusqu'à ce que le nombre de réponses reçues atteigne zéro. À ce moment-là, nous fermons à moitié la communication côté client en utilisant writesDone.

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() ; }
Exécution du code

La logique du serveur renvoie simplement un message dès qu'il lit quelque chose, en créant une réponse avec le numéro de la requête. Dans main, nous créons une telle requête, qui sert finalement de compteur.

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

Un résultat possible de l'exécution du flux bidirectionnel pourrait ressembler à ceci :

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

Exemple de projet @ code.qt.io

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