Qt GRPC Guide du client
Le guide client de l’ Qt GRPC.
Méthodes de service
Dans gRPC™, les méthodes de service peuvent être définies dans un schéma Protobuf afin de spécifier la communication entre les clients et les serveurs. Le compilateur Protobuf, protoc, peut alors générer les interfaces serveur et client requises à partir de ces définitions. gRPC prend en charge quatre types de méthodes de service :
- Appels unaires — Le client envoie une seule requête et reçoit une seule réponse.
rpc UnaryCall (Request) returns (Response);Le gestionnaire client correspondant est QGrpcCallReply.
- Diffusion côté 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 client correspondant est QGrpcClientStream.
- Flux 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 est toujours initiée par le client, qui lance l’appel de procédure à distance (RPC) en envoyant le premier message au serveur. Le serveur met ensuite fin à tout type de communication en renvoyant une StatusCode.
Tous les gestionnaires RPC côté client sont dérivés de la classe ` QGrpcOperation `, qui fournit des fonctionnalités communes. En raison de la nature asynchrone des RPC, ceux-ci sont naturellement gérés via le mécanisme « Signaux et slots » de Qt.
Un signal clé commun à tous les gestionnaires RPC est ` finished`, qui indique l’achèvement d’un RPC. Le gestionnaire émet ce signal exactement une fois au cours de son cycle de vie. Ce signal transmet l’ QGrpcStatus correspondant, fournissant des informations supplémentaires sur la réussite ou l’échec du RPC.
Il existe également des fonctionnalités spécifiques à certaines opérations, telles que ` messageReceived ` pour les messages entrants, ` writeMessage ` pour l’envoi de messages au serveur et ` writesDone ` pour la fermeture de la communication côté client. Le tableau ci-dessous présente les fonctionnalités prises en charge par les gestionnaires de client RPC :
| Fonctionnalité | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
|---|---|---|---|---|
| finished | ✓ (read réponse finale) | ✓ | ✓ (read r la réponse finale) | ✓ |
| messageReceived | ✗ | ✓ | ✗ | ✓ |
| writeMessage | ✗ | ✗ | ✓ | ✓ |
| writesDone | ✗ | ✗ | ✓ | ✓ |
Pour commencer
Pour utiliser l'API C++ d'Qt GRPC, commencez par utiliser un schéma protobuf déjà disponible ou définissez le vôtre. Nous allons utiliser 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 avec notre client Qt GRPC en C++, nous devons exécuter le compilateur protoc avec les plugins de génération Qt. Heureusement, Qt fournit les fonctions CMake `qt_add_grpc ` et `qt_add_protobuf ` pour simplifier 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 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)Cela entraîne la génération de deux fichiers d'en-tête dans le répertoire de compilation actuel :
- clientguide.qpb.h: généré par qtprotobufgen. Déclare les messages protobuf
RequestetResponseà partir du schéma. - clientguide_client.grpc.qpb.h: généré par qtgrpcgen. Déclare l’interface client permettant d’appeler les méthodes d’un serveur « gRPC » implémentant «
ClientGuideService» à partir 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::guideRemarque : il incombeaux utilisateurs de gérer les gestionnaires RPC uniques renvoyés par l’interface Client, en s’assurant de leur existence 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 l'interface ClientGuideService suit une approche simple. Elle valide le champ time du message de requête et renvoie le code d'état INVALID_ARGUMENT si l'heure est postérieure à la date actuelle :
const auto time = now();
if (request->time() > time)
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };De plus, 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 requêtes time valides, les méthodes du service se comportent comme suit :
UnaryCall: Renvoie le champ «num» de la requête.ServerStreaming: Envoie des réponses «num» correspondant au message de requête.ClientStreaming: Compte le nombre de messages de requête et définit ce nombre comme «num».BidirectionalStreaming: Répond immédiatement avec le champ «num» de chaque message de requête 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 ` afin de gérer l’ensemble des communications, ce qui rendra le code plus facile à suivre. Nous commençons par mettre en place la structure de base de toutes les communications dans ` gRPC ` : un canal.
channel = std::make_shared<QGrpcHttp2Channel>(
QUrl("http://localhost:50056")
/* without channel options. */
);
ClientGuide clientGuide(channel);La bibliothèque Qt GRPC propose une méthode ` QGrpcHttp2Channel`, que vous pouvez utiliser via ` attach ` pour accéder à l’interface client générée :
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
{
m_client.attachChannel(std::move(channel));
}Avec cette configuration, le client communiquera via HTTP/2 en utilisant TCP comme protocole de transport. La communication ne sera pas chiffrée (c'est-à-dire sans configuration SSL/TLS).
Création d’un message de requête
Voici un wrapper simple permettant de créer des messages de requête :
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;
}Cette fonction prend un entier et un booléen facultatif. Par défaut, ses messages utilisent l'heure actuelle ; ils devraient donc être acceptés par la logique du serveur. Cependant, lorsqu’elle est appelée avec l’ fail e défini sur true, elle génère des messages que le serveur doit rejeter.
RPC à utilisation unique
Il existe différents paradigmes pour travailler avec les gestionnaires de clients RPC. Plus précisément, vous pouvez opter pour une conception basée sur les classes, dans laquelle le gestionnaire RPC est un membre de la classe qui l'encadre, ou vous pouvez gérer la durée de vie du gestionnaire RPC via le signal ` finished `.
Il y a deux points importants à retenir lors de l'application du paradigme « single-shot ». Le code ci-dessous illustre son fonctionnement pour les appels unaires, mais cela s'applique de la même manière à 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: Étant donné que nous gérons la durée de vie de l'objet RPC unique au sein du lambda, le déplacer dans la capture du lambda invaliderait `
get()` et les 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 « single-shot ». Il est important de marquer cette connexion comme « SingleShotConnection » ! Sinon, la capture de `
reply` ne sera pas détruite, ce qui entraînera une fuite de mémoire cachée difficile à détecter.
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édure à distance
Appels unaires
Les appels unaires ne nécessitent que la gestion du signal « finished ». Lorsque ce signal est émis, nous pouvons vérifier l’ status de l’appel RPC afin de déterminer s’il a abouti. Si tel est le cas, nous pouvons « read » la réponse unique et finale provenant du serveur.
Dans cet exemple, nous utilisons le paradigme « single-shot ». Veillez à lire attentivement la section « RPC single-shot ».
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;
sinon
qDebug("Client (UnaryCall) deserialization failed");
} else {
qDebug() << "Client (UnaryCall) failed:" << status;
}
},
Qt::SingleShotConnection);
}La fonction lance le RPC en appelant la fonction membre ` UnaryCall ` de l’interface client générée ` m_client`. La durée de vie est gérée exclusivement par le signal ` finished `.
Exécution du code
Dans ` main`, nous appelons simplement cette fonction trois fois, en laissant le deuxième appel échouer :
clientGuide.unaryCall(ClientGuide::createRequest(1));
clientGuide.unaryCall(ClientGuide::createRequest(2, ExpectedResult::Failure));
clientGuide.unaryCall(ClientGuide::createRequest(3));Le résultat de l’exécution pourrait ressembler à ceci :
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 )On constate que le serveur reçoit les trois messages, le deuxième contenant une valeur élevée pour son horodatage. Côté client, le premier et le dernier appel ont renvoyé un code d’état « Ok », mais le deuxième message a échoué avec le code d’état « InvalidArgument » car l’horodatage du message se situait dans le futur.
Diffusion en continu sur le serveur
Dans un flux 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 RPC en flux continu. Veillez à lire attentivement la section « RPC single-shot ».
Comme pour tout 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();
connect(
streamPtr, &QGrpcServerStream::finished, streamPtr,
[stream = std::move(stream)](const QGrpcStatus&status) {
if (status.isOk())
qDebug("Client (ServerStreaming) finished");
sinon
qDebug() << "Client (ServerStreaming) failed:" << status;
},
Qt::SingleShotConnection);Pour traiter les messages du serveur, nous nous connectons au signal messageReceived et utilisons la fonction read pour récupérer la réponse lorsque le signal est émis.
connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] {
if (const auto response = streamPtr->read<guide::Response>())
qDebug() << "Client (ServerStream) received:" << *response;
sinon
qDebug("Client (ServerStream) deserialization failed");
});
}Exécution du code
La logique du serveur renvoie au client le montant reçu dans la requête initiale. Nous créons une telle requête et appelons la fonction.
clientGuide.serverStreaming(ClientGuide::createRequest(3));Voici à quoi pourrait ressembler le résultat de l'exécution du serveur en mode streaming :
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) finishedUne fois le serveur démarré, il reçoit une requête avec une valeur « num » égale à 3 et répond par trois messages « Response » avant de mettre fin à la communication.
Diffusion côté client
Dans un flux client, le client envoie une ou plusieurs requêtes, et le serveur répond par une seule réponse finale. Le signal « finished » doit être géré, 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 terminé d’écrire et qu’aucun autre message ne sera envoyé.
Nous utilisons une approche basée sur les classes pour interagir avec le RPC en continu, en intégrant 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();
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:"
<< *réponse;
}
m_clientStream.reset();
} else {
qDebug() << "Client (ClientStreaming) failed:" << status;
qDebug("Restarting the client stream");
clientStreaming(createRequest(0));
}
});
}La fonction lance 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 RPC en flux réussit, nous récupérons la réponse finale du serveur via ` read ` et ` reset ` l'objet RPC. Si le RPC échoue, nous réessayons en appelant la même fonction, ce qui écrase le membre ` m_clientStream ` et reconnecte le signal ` finished `. Nous ne pouvons pas simplement réaffecter le membre ` m_clientStream ` au sein du lambda, car cela entraînerait la perte de la connexion nécessaire.
Exécution du code
Dans main, nous appelons la fonction clientStreaming avec un message d'erreur, ce qui déclenche un échec du RPC et exécute la logique de nouvelle tentative.
clientGuide.clientStreaming(ClientGuide::createRequest(0, ExpectedResult::Failure));Voici à quoi pourrait ressembler le résultat de l’exécution du streaming client :
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 du RPC, déclenchant ainsi la logique de réessai. Le réessai lance le RPC avec un message valide, après quoi trois messages sont envoyés au serveur avant que l’opération ne se termine correctement.
Streaming bidirectionnel
Le streaming bidirectionnel offre la plus grande flexibilité, permettant à la fois 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 via « writeMessage ».
Nous utilisons une approche basée sur les classes avec des connexions via des slots de fonctions membres pour illustrer cette fonctionnalité, en intégrant le gestionnaire en tant que membre de la classe. De plus, nous utilisons la fonction « read » basée sur des pointeurs. 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 connectons les fonctions de slot aux signaux respectifs ` finished ` et ` 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);
}Le fonctionnement des slots est simple. Le slot « finished » affiche simplement l’objet RPC et le réinitialise :
void bidiFinished(const QGrpcStatus&status)
{
if (status.isOk())
qDebug("Client (BidirectionalStreaming) finished");
sinon
qDebug() << "Client (BidirectionalStreaming) failed:" << status;
m_bidiStream.reset();
}Le slot messageReceived read les messages dans le membre m_bidiResponse, et continue d’écrire des messages jusqu’à ce que le compteur de réponses reçues atteigne zéro. À ce stade, nous effectuons une demi-fermeture de la communication côté client à l’aide de 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 se contente de renvoyer un message dès qu’elle lit quelque chose, en créant une réponse contenant le nombre figurant dans la requête. Dans main, nous créons une telle requête, qui sert en fin de compte de compteur.
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));Voici à quoi pourrait ressembler le résultat de l'exécution du streaming bidirectionnel :
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) finishedIntercepteurs ClientGuide
Pour une introduction au mécanisme d’interception et au rôle des interfaces d’intercepteurs dans le cycle de vie d’un RPC, consultez la présentation des intercepteurs d’Qt GRPC .
L'exemple du guide client illustre trois intercepteurs combinés en une seule chaîne :
- Un intercepteur de métriques qui enregistre les statistiques RPC et injecte un identifiant de trace.
- Un intercepteur d’authentification qui associe un jeton « bearer ».
- Un intercepteur de nettoyage qui valide et normalise les messages.
Ensemble, ils illustrent comment les intercepteurs peuvent observer, modifier et valider le trafic RPC à différentes étapes du cycle de vie de l'appel.
Intercepteur de métriques
L'intercepteur de métriques est placé en premier dans la chaîne afin de pouvoir observer l'intégralité du cycle de vie du RPC. Il enregistre des statistiques de base telles que la taille des charges utiles reçues, la durée des appels RPC et le nombre de succès ou d'échecs pour chaque méthode RPC.
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;
};Implémentation de l’intercepteur de métriques
Lorsqu’un RPC démarre, l’intercepteur enregistre l’horodatage actuel et le stocke en utilisant l’identifiant de trace unique ( operationId ) comme clé. Un identifiant de trace est également ajouté aux métadonnées sortantes afin que l’appel puisse être corrélé entre les différents services.
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);
}Tant que le RPC est actif, les messages entrants contribuent aux métriques collectées en cumulant le nombre d’octets de charge utile reçus.
À la fin du RPC, l’intercepteur calcule la durée totale et met à jour les statistiques agrégées par descripteur de RPC.
À des fins de démonstration, les métriques collectées sont affichées lorsque l'instance de l'intercepteur est détruite.
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;
}
}Intercepteur d'authentification
L'intercepteur d'authentification ajoute un jeton « bearer » aux appels RPC sortants. Si l'application fournit déjà un en-tête « authorization », l'intercepteur le laisse tel quel.
Si le serveur signale un échec d'authentification, l'intercepteur actualise le jeton afin que les appels RPC suivants utilisent des identifiants mis à jour.
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;
};Implémentation de l'intercepteur d'authentification
L’ TokenProvider encapsule la gestion des jetons. Dans cet exemple, il génère une chaîne de caractères simplifiée représentant un jeton, à des fins de démonstration uniquement. Elle ne correspond pas à un véritable jeton d’authentification. Dans les applications réelles, les jetons seraient généralement obtenus et actualisés via un système d’identité externe chargé d’émettre et de gérer les jetons d’authentification.
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;
}Côté serveur, les appels entrants valident les métadonnées de l’ authorization. En cas d’échec de la validation, le serveur renvoie un statut « Unauthenticated » et fournit des informations supplémentaires dans les métadonnées de fin.
if (auto authStatus = validateAuth(context); !authStatus.ok())
return authStatus;Lorsqu’un RPC démarre, l’intercepteur vérifie si l’appel contient déjà un en-tête authorization. Si ce n’est pas le cas, il ajoute un jeton de support aux métadonnées sortantes.
Les métadonnées de fin sont inspectées à la recherche d’indications d’authentification renvoyées par le serveur. Si elles sont présentes, ces indications sont stockées afin de pouvoir être signalées si l’appel échoue finalement en raison d’une erreur d’authentification.
Lorsque le RPC se termine avec un statut « Unauthenticated », l’intercepteur actualise le jeton. Dans cet exemple, l’actualisation consiste simplement à régénérer un jeton avec une nouvelle heure actuelle. Dans des scénarios réels, cela impliquerait de demander un nouveau jeton à un système d’identité externe.
QGrpcStartInterceptor::Continuation
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)
{
// Ne traiter que les codes d'état « Non authentifié ».
if (status.code() != QtGrpc::StatusCode::Unauthenticated)
return;
qDebug("[AuthInterceptor] Refreshing token for next call");
// REMARQUE : Exemple simplifié. Les jetons réels sont obtenus via un processus d'authentification approprié.
m_tokenProvider.refresh();
if (!m_serverAuthHint.isEmpty()) {
status = QGrpcStatus{ status.code(), status.message() + u", Indice : "_s + m_serverAuthHint };
m_serverAuthHint.clear();
}
}Intercepteur de nettoyage
L’intercepteur de nettoyage valide et normalise les données de requête et de réponse. Il montre comment les intercepteurs peuvent appliquer des contraintes de base côté client avant l’envoi des messages et après la réception des réponses.
Dans cet exemple, l’intercepteur limite les valeurs des requêtes à des plages acceptables, normalise les horodatages et rejette les appels dont les métadonnées dépassent une limite de taille prédéfinie.
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;
};Implémentation de l'intercepteur de nettoyage
Des fonctions d'aide sont utilisées pour calculer la taille totale des métadonnées et pour appliquer des règles de normalisation de base aux messages de requête et de réponse.
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));
}Lorsqu’un RPC démarre, l’intercepteur vérifie d’abord que les métadonnées sortantes ne dépassent pas la taille autorisée. Si la limite est dépassée, l’appel est interrompu et renvoie Continuation::Drop
Si le message de requête correspond au type attendu, ses champs sont nettoyés avant l’envoi de l’appel RPC.
Pour les appels en flux continu, les messages sortants sont également nettoyés dans le hook ` onWriteMessage `.
Enfin, les réponses entrantes sont interceptées. La charge utile sérialisée est désérialisée, normalisée, puis sérialisée à nouveau avant d’être transmise à l’application.
QGrpcStartInterceptor::Continuation
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)
{
// Pour gérer des scénarios plus complexes impliquant différents types de messages de requête,
// il serait judicieux d'effectuer une branche en fonction de context.descriptor() de manière appropriée.
if (auto *request = qprotobufmessage_cast<client::guide::Request *>(&message))
sanitizeMessage(*request);
}
void SanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext&context,
QByteArray&messageData)
{
// messageData ne peut être que de type client::guide::Response. Pour une
// gestion plus complexe, ajoutez une logique appropriée via context.descriptor().
// Notez que la désérialisation et la sérialisation entraînent une surcharge.
client::guide::Response response;
auto serializer = context.channel().serializer();
if (serializer->deserialize(&response, messageData))
return;
sanitizeMessage(response);
messageData = serializer->serialize(&response);
}Utilisation de l'intercepteur
Construisez la chaîne d’intercepteurs dans une fonction d’aide et renvoyez-la à l’appelant.
inline QGrpcInterceptorChain createInterceptors()
{
QGrpcInterceptorChain chain;
chain.set(
std::make_unique<MetricsInterceptor>(),
std::make_unique<AuthInterceptor>(),
std::make_unique<SanitizingInterceptor>()
);
return chain;
}Avant d'associer la chaîne au canal, vérifiez qu'elle a bien été initialisée (par exemple en vérifiant isEmpty()) et appliquez une stratégie de gestion des erreurs appropriée en cas d'échec de la création.
auto interceptors = createInterceptors();
if (interceptors.isEmpty()) {
qWarning("Failed to create the interceptor chain");
return EXIT_FAILURE; // ou toute autre solution de secours appropriée
}
channel = std::make_shared<QGrpcHttp2Channel>(
QUrl("http://localhost:50056"),
std::move(interceptors)
);Pour les appels unaires, l’exemple planifie une séquence de requêtes avec un court délai entre chacune d’elles. Cela permet d’observer plus facilement comment les intercepteurs affectent les appels successifs : des métadonnées de trace sont ajoutées, les échecs d’authentification actualisent le jeton pour les requêtes suivantes, et des métadonnées non valides peuvent entraîner l’abandon d’un appel avant même qu’il ne soit envoyé.
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;L’exemple effectue également un RPC en streaming bidirectionnel. L’implémentation de ` bidirectionalStreaming() ` échangerait normalement des messages ` 666 `, mais l’intercepteur de nettoyage limite les valeurs des requêtes de sorte que le flux s’arrête après cinq échanges.
QTimer::singleShot(delayMs, [&] {
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(666));
});Exécution du code
Exécutez clientguide_client avec l’option -I pour activer la prise en charge des intercepteurs. Voici un exemple de résultat :
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)La sortie montre la chaîne d’intercepteurs en action. Les appels unaires sortants parviennent au serveur avec des métadonnées de trace ajoutées par l’intercepteur de métriques. Lorsque le serveur signale un échec d’authentification, l’intercepteur d’authentification actualise le jeton pour les appels suivants et enrichit le message d’erreur avec l’indication fournie par le serveur. Un appel ultérieur est rejeté localement par l’intercepteur de nettoyage car ses métadonnées dépassent la limite configurée. L’appel de streaming bidirectionnel montre que les intercepteurs s’appliquent également aux RPC en streaming : l’intercepteur de nettoyage limite la valeur de la requête, de sorte que le flux s’arrête après cinq échanges de messages au lieu de se poursuivre à partir de 666. Enfin, lorsque le client s’arrête, l’intercepteur de métriques affiche les statistiques collectées pour chaque méthode RPC.
© 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.