Chat QtGrpc
Une application de chat permettant de partager des messages de toute nature dans un salon de discussion.
L'exemple Chat illustre une utilisation avancée de l'API client Qt GRPC. Le serveur permet aux utilisateurs de s'inscrire et de s'authentifier, ce qui leur donne accès à la salle de discussion (ChatRoom). Une fois connectés, les utilisateurs peuvent partager divers types de messages dans la salle de discussion, tels que des messages texte, des images, l'activité des utilisateurs ou tout autre fichier provenant de leur disque dur, avec tous les autres participants.

Exécution de l’exemple
- Assurez-vous que le service
qtgrpc_chat_serverest en cours d’exécution et qu’il est bien à l’écoute. - Si vous êtes sur la même machine que le serveur, l'adresse par défaut de l'
localhostdevrait suffire lorsque vous exécutez la commande `qtgrpc_chat_client`. Si vous utilisez un appareil autre que celui hébergeant le serveur, indiquez l'adresse IP correcte de l'hôte exécutant le serveur dans la boîte de dialogue Paramètres. - Assurez-vous que l’option CMake «
GRPC_CHAT_USE_EMOJI_FONT» est activée sur le client pour bénéficier d’une expérience fluide avec les emojis 🚀.

Pour exécuter l’exemple à partir de Qt Creator, ouvrez le mode « Welcome » et sélectionnez l’exemple sur Examples. Pour plus d’informations, consultez Qt Creator: Tutoriel : Compiler et exécuter.
Modules et classes concernés.
Cet exemple présente les modules et classes Qt suivants.
- Communication via une connexion de longue durée QGrpcBidiStream.
- Utilisation du client
QtGrpcdepuis un worker thread. - Utilisation du module QtProtobufQtCoreTypes dans le schéma Protobuf.
- Communication sécurisée via SSL.
- Visualisation des messages d'QtProtobuf dans un ListView QML.
Schéma Protobuf
Le schéma Protobuf définit la structure des messages et des services utilisés dans l'application de chat. Le schéma est divisé en deux fichiers :
syntax = "proto3";
package chat;
import "chatmessages.proto";
service QtGrpcChat {
// Register a user with \a Credentials.
rpc Register(Credentials) returns (None);
// Join as a registered user and exchange \a ChatMessage(s)
rpc ChatRoom(stream ChatMessage) returns (stream ChatMessage) {}
}Le fichier ` qtgrpcchat.proto ` spécifie le service `QtGrpcChat`, qui fournit deux méthodes RPC :
Register: Enregistre un utilisateur à l’aide de l’Credentialsfournie. Le serveur stocke et vérifie les utilisateurs à partir d’une base de données en texte clair.ChatRoom: Établit un flux bidirectionnel permettant l’échange d’ChatMessages entre tous les clients connectés. Le serveur diffuse tous les messages entrants vers les autres clients connectés.
syntax = "proto3";
package chat;
import "QtCore/QtCore.proto";
message ChatMessage {
string username = 1;
int64 timestamp = 2;
oneof content {
TextMessage text = 3;
FileMessage file = 4;
UserStatus user_status = 5;
}
}Le fichier d’ chatmessages.proto s définit ChatMessage, qui est une union balisée (également appelée « type somme »). Elle représente tous les messages individuels pouvant être envoyés via le RPC en flux continu ChatRoom. Chaque ChatMessage doit inclure une username et une timestamp pour identifier l’expéditeur.
Vous incluez l’importation QtCore/QtCore.proto pour activer les types du module QtProtobufQtCoreTypes, ce qui permet une conversion transparente entre les types spécifiques à QtCore et leurs équivalents Protobuf.
message FileMessage {
enum Type {
UNKNOWN = 0;
IMAGE = 1;
AUDIO = 2;
VIDEO = 3;
TEXT = 4;
}
Type type = 1;
string name = 2;
bytes content = 3;
uint64 size = 4;
message Continuation {
uint64 index = 1;
uint64 count = 2;
QtCore.QUuid uuid = 3;
}
optional Continuation continuation = 5;
}FileMessage est l’un des types de message pris en charge pour le type somme ChatMessage. Il permet d’encapsuler n’importe quel fichier local dans un message. Le champ optionnel Continuation garantit une transmission fiable en gérant les transferts de fichiers volumineux par blocs.
Remarque : pour plus de détails sur l’utilisation du module ProtobufQtCoreTypes dans votre schéma Protobuf et votre code d’application, consultez Qt Core usage.
Serveur
Remarque : l'application serveur décrite ici utilise la gRPC™ bibliothèque.
L'application serveur utilise l'API de rappel asynchrone gRPC. Cela nous permet de bénéficier des avantages en termes de performances de l'API asynchrone sans avoir à gérer manuellement les files d'attente d'exécution.
class QtGrpcChatService final : public chat::QtGrpcChat::CallbackServiceVous déclarez la classe ` QtGrpcChatService `, qui est une sous-classe de la classe ` CallbackService ` du service ` QtGrpcChat ` généré.
grpc::ServerBidiReactor<chat::ChatMessage, chat::ChatMessage> *
ChatRoom(grpc::CallbackServerContext *context) override
{
return new ChatRoomReactor(this, context);
}
grpc::ServerUnaryReactor *Register(grpc::CallbackServerContext *context,
const chat::Credentials *request,
chat::None * /*response*/) overrideRedéfinissez également les fonctions virtuelles afin d’implémenter les fonctionnalités des deux méthodes gRPC fournies par le service :
- La méthode
Registervérifie et stocke les utilisateurs dans une base de données en texte clair. - La méthode
ChatRoomvérifie les identifiants fournis dans les métadonnées par rapport à la base de données. En cas de succès, elle établit un flux bidirectionnel pour la communication.
// Broadcast \a message to all connected clients. Optionally \a skip a client
void broadcast(const std::shared_ptr<chat::ChatMessage> &message, const ChatRoomReactor *skip)
{
for (auto *client : activeClients()) {
assert(client);
if (skip && client == skip)
continue;
client->startSharedWrite(message);
}
}L’implémentation du service assure le suivi de tous les clients actifs qui se connectent ou se déconnectent via la méthode ` ChatRoom `. Cela permet d’activer la fonctionnalité « broadcast », qui diffuse des messages à tous les clients connectés. Afin de réduire l’espace de stockage et la charge système, l’interface ` ChatMessage ` est encapsulée dans une interface ` shared_ptr`.
// Share \a response. It will be kept alive until the last write operation finishes.
void startSharedWrite(std::shared_ptr<chat::ChatMessage> response)
{
std::scoped_lock lock(m_writeMtx);
if (m_response) {
m_responseQueue.emplace(std::move(response));
} else {
m_response = std::move(response);
StartWrite(m_response.get());
}
}La méthode ` startSharedWrite ` est une fonction membre de ` ChatRoomReactor`. Si le réacteur est en cours d’écriture, le message est mis en mémoire tampon dans une file d’attente. Sinon, une opération d’écriture est lancée. Il existe un message unique partagé entre tous les clients. Chaque copie du message ` response ` augmente la valeur de ` use_count`. Une fois que tous les clients ont fini d’écrire le message et que son ` use_count ` tombe à 0, ses ressources sont libérées.
// Distribute the incoming message to all other clients.
m_service->broadcast(m_request, this);
m_request = std::make_shared<chat::ChatMessage>(); // detach
StartRead(m_request.get());Cet extrait de code fait partie de la méthode virtuelle ChatRoomReactor::OnReadDone. Chaque fois que cette méthode est appelée, un nouveau message a été reçu du client. Le message est diffusé à tous les autres clients, en ignorant l'expéditeur.
std::scoped_lock lock(m_writeMtx);
if (!m_responseQueue.empty()) {
m_response = std::move(m_responseQueue.front());
m_responseQueue.pop();
StartWrite(m_response.get());
return;
}
m_response.reset();Cet extrait de code fait partie de la méthode virtuelle ` ChatRoomReactor::OnWriteDone `. Chaque fois que cette méthode est appelée, un message a été écrit vers le client. S’il y a des messages mis en mémoire tampon dans la file d’attente, le message suivant est écrit. Sinon, ` m_response ` est réinitialisé pour signaler qu’aucune opération d’écriture n’est en cours. Un verrou est utilisé pour éviter tout conflit avec la méthode ` broadcast `.
Client
L’application cliente utilise le schéma Protobuf fourni pour communiquer avec le serveur. Elle offre à la fois des fonctionnalités front-end et back-end pour l’enregistrement des utilisateurs et la gestion du flux bidirectionnel de longue durée de la méthode ChatRoom gRPC . Cela permet la visualisation et la communication des ChatMessages.
Configuration
add_library(qtgrpc_chat_client_proto STATIC)
qt_add_protobuf(qtgrpc_chat_client_proto
QML
QML_URI QtGrpcChat.Proto
PROTO_FILES
../proto/chatmessages.proto
PROTO_INCLUDES
$<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)
qt_add_grpc(qtgrpc_chat_client_proto CLIENT
PROTO_FILES
../proto/qtgrpcchat.proto
PROTO_INCLUDES
$<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)Commencez par générer les fichiers source à partir du schéma Protobuf. Étant donné que le fichier qtgrpcchat.proto ne contient aucune définition d’ message, seule la génération par qtgrpcgen est nécessaire. Fournissez également l’ PROTO_INCLUDES du module ProtobufQtCoreTypes afin de garantir la validité de l’importation "QtCore/QtCore.proto".
target_link_libraries(qtgrpc_chat_client_proto
PUBLIC
Qt6::Protobuf
Qt6::ProtobufQtCoreTypes
Qt6::Grpc
)Assurez-vous que la cible indépendante qtgrpc_chat_client_proto soit liée publiquement à ses dépendances, y compris le module ProtobufQtCoreTypes. La cible de l’application est ensuite liée à cette bibliothèque.
Logique du backend
Le backend de l'application s'articule autour de quatre éléments essentiels :
ChatEngine: un singleton orienté QML qui gère la logique de l'application.ClientWorker: Un objet « worker » qui fournit de manière asynchrone des fonctionnalités client d’ gRPC.ChatMessageModel: Un objet d'QAbstractListModelpersonnalisé chargé de gérer et de stocker les sessions d'ChatMessage.UserStatusModel: UneQAbstractListModelpersonnalisée pour la gestion de l'activité des utilisateurs.
explicit ChatEngine(QObject *parent = nullptr);
~ChatEngine() override;
// Register operations
Q_INVOKABLE void registerUser(const chat::Credentials &credentials);
// ChatRoom operations
Q_INVOKABLE void login(const chat::Credentials &credentials);
Q_INVOKABLE void logout();
Q_INVOKABLE void sendText(const QString &message);
Q_INVOKABLE void sendFile(const QUrl &url);
Q_INVOKABLE void sendFiles(const QList<QUrl> &urls);
Q_INVOKABLE bool sendFilesFromClipboard();L'extrait ci-dessus illustre certaines des fonctionnalités d'Q_INVOKABLE appelées depuis QML pour interagir avec le serveur.
explicit ClientWorker(std::shared_ptr<LogModel> logger, QObject *parent = nullptr);
~ClientWorker() override;
public Q_SLOTS:
void registerUser(const chat::Credentials &credentials);
void login(const chat::Credentials &credentials);
void logout();
void sendFile(const QUrl &url);
void sendFiles(const QList<QUrl> &urls);
void sendMessage(const chat::ChatMessage &message);Les slots fournis par l’ ClientWorker reflètent en partie l’API exposée par l’ ChatEngine. L’ ClientWorker fonctionne dans un thread dédié afin de gérer en arrière-plan les opérations gourmandes en ressources, telles que la transmission ou la réception de fichiers volumineux.
m_clientWorker->moveToThread(&m_clientThread);
m_clientThread.start();
connect(&m_clientThread, &QThread::finished, m_clientWorker, &QObject::deleteLater);
connect(m_clientWorker, &ClientWorker::registerFinished, this, &ChatEngine::registerFinished);
connect(m_clientWorker, &ClientWorker::chatError, this, &ChatEngine::chatError);
...Dans le constructeur de ChatEngine, attribuez l’ ClientWorker à son thread de travail dédié et continuez à traiter et à transmettre ses signaux afin de les rendre disponibles côté QML.
void ChatEngine::registerUser(const chat::Credentials &credentials)
{
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::registerUser, credentials);
}
...
void ClientWorker::registerUser(const chat::Credentials &credentials)
{
if (credentials.name().isEmpty() || credentials.password().isEmpty()) {
emit chatError(tr("Invalid credentials for registration"));
return;
}
if ((!m_client || m_hostUriDirty) && !initializeClient()) {
emit chatError(tr("Failed registration: unabled to initialize client"));
return;
}
auto reply = m_client->Register(credentials, QGrpcCallOptions{}.setDeadlineTimeout(2s));
const auto *replyPtr = reply.get();
connect(
replyPtr, &QGrpcCallReply::finished, this,
[this, reply = std::move(reply)](const QGrpcStatus &status) {
emit registerFinished(status);
},
Qt::SingleShotConnection);
}Ceci illustre la manière dont l’ ChatEngine interagit avec l’ ClientWorker pour enregistrer les utilisateurs. Étant donné que l’ ClientWorker s’exécute dans son propre thread, il est important d’utiliser ` invokeMethod ` pour appeler ses fonctions membres en toute sécurité.
Dans la méthode ` ClientWorker`, vous vérifiez si le client n’est pas initialisé ou si l’URI de l’hôte a changé. Si l’une de ces conditions est remplie, appelez ` initializeClient`, qui crée un nouvel objet ` QGrpcHttp2Channel`. Comme il s’agit d’une opération coûteuse, limitez au maximum son exécution.
Pour gérer le RPC ` Register `, utilisez l’option ` setDeadlineTimeout ` afin de vous prémunir contre l’inactivité du serveur. Il est généralement recommandé de définir un délai pour les RPC unaires.
void ClientWorker::login(const chat::Credentials &credentials)
{
if (credentials.name().isEmpty() || credentials.password().isEmpty()) {
emit chatError(tr("Invalid credentials for login"));
return;
}
...
QGrpcCallOptions opts;
opts.setMetadata({
{ "user-name", credentials.name().toUtf8() },
{ "user-password", credentials.password().toUtf8() },
});
connectStream(opts);
}Lors de la connexion à l'ChatRoom, vous pouvez utiliser l'option setMetadata pour fournir les identifiants de l'utilisateur, comme l'exige le serveur pour l'authentification. L'appel proprement dit et l'établissement de la connexion sont gérés dans la méthode connectStream.
void ClientWorker::connectStream(const QGrpcCallOptions &opts)
{
...
m_chatStream = m_client->ChatRoom(*initialMessage, opts);
...
connect(m_chatStream.get(), &QGrpcBidiStream::finished, this,
[this, opts](const QGrpcStatus &status) {
if (m_chatState == ChatState::Connected) {
// If we're connected retry again in 250 ms, no matter the error.
QTimer::singleShot(250, [this, opts]() { connectStream(opts); });
} else {
setState(ChatState::Disconnected);
m_chatResponse = {};
m_userCredentials = {};
m_chatStream.reset();
emit chatStreamFinished(status);
}
});
...Pour implémenter une logique de reconnexion de base au cas où le flux se terminerait brusquement alors que vous êtes encore connecté, il suffit d’appeler à nouveau connectStream en utilisant l’ QGrpcCallOptions de l’appel initial. Cela garantit que toutes les connexions requises sont également mises à jour.
Remarque : le mode Doze/App-Standbyd’Android peut être déclenché, par exemple en utilisant la boîte de dialogue FileDialog ou en basculant vers une autre application. Ce mode désactive l’accès au réseau, fermant toutes les connexions QTcpSocket actives et entraînant la mise en état « finished » du flux. Vous pouvez résoudre ce problème à l’aide de la logique de reconnexion.
connect(m_chatStream.get(), &QGrpcBidiStream::messageReceived, this, [this] {
...
switch (m_chatResponse.contentField()) {
case chat::ChatMessage::ContentFields::UninitializedField:
qDebug("Received uninitialized message");
return;
case chat::ChatMessage::ContentFields::Text :
if (m_chatResponse.text().content().isEmpty())
return;
break;
case chat::ChatMessage::ContentFields::File :
// Télécharger les messages contenant des fichiers et stocker l'URL de téléchargement dans le
// contenu, afin que le modèle puisse y faire référence.
m_chatResponse.file()
.setContent(saveFileRequest(m_chatResponse.file()).toString().toUtf8());
break;
...
emit chatStreamMessageReceived(m_chatResponse);
});
setState(Backend::ChatState::Connecting);
}Lorsque des messages sont reçus, le composant « ClientWorker » effectue un prétraitement, tel que l’enregistrement du contenu de l’ FileMessage, afin que le composant « ChatEngine » n’ait plus qu’à se concentrer sur les modèles. Utilisez l’énumération « ContentFields » pour vérifier en toute sécurité le champ « oneof content » de notre type somme « ChatMessage ».
void ChatEngine::sendText(const QString &message)
{
if (message.trimmed().isEmpty())
return;
if (auto request = m_clientWorker->createMessage()) {
chat::TextMessage tmsg;
tmsg.setContent(message.toUtf8());
request->setText(std::move(tmsg));
QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendMessage, *request);
m_chatMessageModel->appendMessage(*request);
}
}
...
void ClientWorker::sendMessage(const chat::ChatMessage &message)
{
if (!m_chatStream || m_chatState != ChatState::Connected) {
emit chatError(tr("Unable to send message"));
return;
}
m_chatStream->writeMessage(message);
}Lors de l’envoi de messages, l’ ChatEngine crée des requêtes correctement formatées. Par exemple, la méthode sendText accepte un QString et utilise la fonction createMessage pour générer un message valide dont les champs username et timestamp sont renseignés. Le client est ensuite appelé pour envoyer le message, et une copie est mise en file d’attente dans notre propre ChatMessageModel.
Interface QML
import QtGrpc
import QtGrpcChat
import QtGrpcChat.ProtoLes importations suivantes sont utilisées dans le code QML :
QtGrpc: Fournit les fonctionnalités QML d’ QtGrpc, telles que StatusCode.QtGrpcChat: Notre module d’application, qui comprend des composants tels que le singletonChatEngine.QtGrpcChat.Proto: Permet d’accéder en QML à nos types protobuf générés.
Connections {
target: ChatEngine
function onChatStreamFinished(status) {
root.handleStatus(status)
loginView.clear()
}
function onChatStateChanged() {
if (ChatEngine.chatState === Backend.ChatState.Connected && mainView.depth === 1)
mainView.push("ChatView.qml")
else if (ChatEngine.chatState === Backend.ChatState.Disconnected && mainView.depth > 1)
mainView.pop()
}
function onRegisterFinished(status) {
root.handleStatus(status)
}
function onChatError(message) {
statusDisplay.text = message
statusDisplay.color = "yellow"
statusDisplay.restart()
}
}Main.qml gère les signaux principaux émis par l’ ChatEngine. La plupart de ces signaux sont gérés de manière globale et sont visualisés quel que soit l’état de l’application.
Rectangle {
id: root
property credentials creds
...
ColumnLayout {
id: credentialsItem
...
RowLayout {
id: buttonLayout
...
Button {
id: loginButton
...
enabled: nameField.text && passwordField.text
text: qsTr("Login")
onPressed: {
root.creds.name = nameField.text
root.creds.password = passwordField.text
ChatEngine.login(root.creds)
}
}Les types de messages générés à partir du schéma Protobuf sont accessibles en QML, car il s’agit d’ QML_VALUE_TYPEs (une version en camelCase de la définition du message). L’ LoginView.qml utilise la propriété de type de valeur credentials pour lancer l’ login sur l’ ChatEngine.
ListView {
id: chatMessageView
...
component DelegateBase: Item {
id: base
required property chatMessage display
default property alias data: chatLayout.data
...
}
...
// We use the DelegateChooser and the 'whatThis' role to determine
// the correct delegate for any ChatMessage
delegate: DelegateChooser {
role: "whatsThis"
...
DelegateChoice {
roleValue: "text"
delegate: DelegateBase {
id: dbt
TextDelegate {
Layout.fillWidth: true
Layout.maximumWidth: root.maxMessageBoxWidth
Layout.preferredHeight: implicitHeight
Layout.bottomMargin: root.margin
Layout.leftMargin: root.margin
Layout.rightMargin: root.margin
message: dbt.display.text
selectionColor: dbt.lightColor
selectedTextColor: dbt.darkColor
}
}
}Dans ChatView.qml, l’ ListView affiche les messages dans l’ ChatRoom. Cette opération est légèrement plus complexe, car vous devez gérer le type somme ChatMessage de manière conditionnelle.
Vous pouvez utiliser un DelegateChooser, qui permet de sélectionner le délégué approprié en fonction du type de message. Utilisez le rôle par défaut « whatThis » dans le modèle, qui fournit le type de message pour chaque instance de « ChatMessage ». Le composant « DelegateBase » accède ensuite au rôle « display » du modèle, rendant ainsi les données « chatMessage » disponibles pour le rendu.
TextEdit {
id: root
required property textMessage message
text: message.content
color: "#f3f3f3"
font.pointSize: 14
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
}Voici l’un des composants qui visualise le type ` TextMessage `. Il utilise le type de valeur ` textMessage ` du module protobuf pour visualiser le texte.
TextArea.flickable: TextArea {
id: inputField
function sendTextMessage() : void {
if (text === "")
return
ChatEngine.sendText(text)
text = ""
}
...
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return && event.modifiers & Qt.ControlModifier) {
sendTextMessage()
event.accepted = true
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
if (ChatEngine.sendFilesFromClipboard())
event.accepted = true
}
}Le client de chat propose différents moyens d’envoyer des messages, tels que :
- L'acceptation des fichiers déposés dans l'application.
- <Ctrl + V> pour envoyer tout contenu stocké dans le QClipboard.
- <Ctrl + Entrée> pour envoyer le message à partir du
inputField - Cliquer sur le bouton « Envoyer » pour
inputField - Sélectionner des fichiers via une boîte de dialogue de sélection de fichiers
SSL
Pour sécuriser la communication entre le serveur et les clients, le chiffrement SSL/TLS est utilisé. Cela nécessite au minimum les éléments suivants :
- Clé privée: contient la clé privée du serveur, qui sert à établir des connexions sécurisées. Elle doit rester confidentielle et ne doit jamais être partagée.
- Certificat: contient le certificat public du serveur, qui est partagé avec les clients pour vérifier l’identité du serveur. Il est généralement signé par une autorité de certification (CA) ou peut être auto-signé à des fins de test.
- Certificat racine de l'AC(facultatif) : si vous utilisez une autorité de certification (AC) personnalisée pour signer le certificat de votre serveur, le certificat racine de l'AC est requis côté client pour valider la chaîne de certificats du serveur. Cela garantit que le client peut faire confiance au certificat du serveur, car le certificat racine de l’AC personnalisée n’est pas préinstallé dans le magasin de confiance du client, contrairement à ceux des AC publiques.
Utilisez OpenSSL pour créer ces fichiers et configurer la communication de notre gRPC afin d’utiliser SSL/TLS.
grpc::SslServerCredentialsOptions sslOpts;
sslOpts.pem_key_cert_pairs.emplace_back(grpc::SslServerCredentialsOptions::PemKeyCertPair{
LocalhostKey,
LocalhostCert,
});
builder.AddListeningPort(QtGrpcChatService::httpsAddress(), grpc::SslServerCredentials(sslOpts));
builder.AddListeningPort(QtGrpcChatService::httpAddress(), grpc::InsecureServerCredentials());Vous fournissez la clé privée et le certificat au serveur gRPC. Grâce à cela, vous pouvez configurer SslServerCredentials pour activer le protocole TLS côté serveur. En plus de la communication sécurisée, autorisez également l’accès non chiffré.
Le serveur écoute sur les adresses suivantes :
- HTTPS :
0.0.0.0:65002 - HTTP :
0.0.0.0:65003
Le serveur se lie à l'adresse 0.0.0.0 pour écouter sur toutes les interfaces réseau, ce qui permet l'accès depuis n'importe quel appareil connecté au même réseau.
if (m_hostUri.scheme()== "https") {
if (!QSslSocket::supportsSsl()) {
emit chatError(tr("L'appareil ne prend pas en charge le protocole SSL. Veuillez utiliser le schéma « http »."));
return false;
}
QFile crtFile(":/res/root.crt");
if (!crtFile.open(QFile::ReadOnly)) {
qFatal("Unable to load root certificate");
return false;
}
QSslConfiguration sslConfig;
QSslCertificate crt(crtFile.readAll());
sslConfig.addCaCertificate(crt);
sslConfig.setProtocol(QSsl::TlsV1_2OrLater);
sslConfig.setAllowedNextProtocols({ "h2" }); // Autoriser HTTP/2
// Désactiver la vérification du nom d’hôte pour autoriser les connexions depuis n’importe quelle adresse IP locale.
// Acceptable en environnement de développement, mais à éviter en production pour des raisons de sécurité.
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
opts.setSslConfiguration(sslConfig);
}Le client charge le certificat de l'autorité de certification racine, puisque vous avez auto-signé cette autorité. Ce certificat est utilisé pour créer le fichier ` QSslCertificate`. Il est important de spécifier le protocole ` "h2" ` avec ` setAllowedNextProtocols`, puisque vous utilisez HTTP/2.
Intercepteurs
Le client de chat utilise une chaîne de surveillance ( QGrpcInterceptorChain ) pour observer toute l’activité RPC. Lors de l’initialisation, une chaîne de surveillance ( LoggingInterceptor ) s’attache au canal, offrant ainsi à l’application une vue en temps réel de chaque opération transitant par la chaîne.
QGrpcInterceptorChain interceptorChain;
interceptorChain.add(std::make_unique<LoggingInterceptor>(m_logModel));
auto channel = std::make_shared<QGrpcHttp2Channel>(m_hostUri, opts,
std::move(interceptorChain));Vous créez une chaîne d’ QGrpcInterceptorChain s et y ajoutez l’intercepteur à l’aide de sa surcharge unique_ptr, qui transfère la propriété à la chaîne. Vous transmettez ensuite la chaîne au constructeur QGrpcHttp2Channel. Une fois que le canal en a pris possession, l’intercepteur reste valide pendant toute la durée de vie du canal.
class LoggingInterceptor final : public QGrpcStartInterceptor,
public QGrpcInitialMetadataInterceptor,
public QGrpcMessageReceivedInterceptor,
public QGrpcWriteMessageInterceptor,
public QGrpcWritesDoneInterceptor,
public QGrpcTrailingMetadataInterceptor,
public QGrpcCancelInterceptor,
public QGrpcFinishedInterceptor
{
public:
explicit LoggingInterceptor(std::shared_ptr<LogModel> logModel);
~LoggingInterceptor() override;
Continuation onStart(QGrpcInterceptionContext &context, QProtobufMessage &message,
QGrpcCallOptions &callOptions) override;
void onInitialMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) override;
void onMessageReceived(QGrpcInterceptionContext &context, QByteArray &messageData) override;
void onWriteMessage(QGrpcInterceptionContext &context, QProtobufMessage &message) override;
void onWritesDone(QGrpcInterceptionContext &context) override;
void onTrailingMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) override;
void onCancel(QGrpcInterceptionContext &context) override;
void onFinished(QGrpcInterceptionContext &context, QGrpcStatus &status) override;
private:
std::shared_ptr<LogModel> m_log;
using Clock = std::chrono::steady_clock;
using Ms = std::chrono::duration<double, std::milli>;
QHash<quint64, Clock::time_point> m_activeRPCs;
};LoggingInterceptor implémente toutes les interfaces d’intercepteur disponibles, couvrant ainsi l’intégralité du cycle de vie RPC. Il contient une référence shared_ptr vers une instance LogModel permettant d’ajouter des entrées de journal, ainsi qu’une référence QHash permettant de suivre les opérations actives par ID.
Implémentation de l’intercepteur de journalisation
Les extraits de code suivants montrent comment fonctionnent les points d’interception onStart et onFinished. Les autres points d’interception effectuent une journalisation de base de leurs événements respectifs.
LoggingInterceptor::Continuation LoggingInterceptor::onStart(QGrpcInterceptionContext &context,
QProtobufMessage &, QGrpcCallOptions &)
{
const auto id = context.operationId();
m_activeRPCs.insert(id, Clock::now());
m_log->add(LogModel::Level::Debug, id, context.descriptor(), u"Starting"_s);
return Continuation::Proceed;
}Dans ` onStart`, l’intercepteur stocke l’horodatage actuel dans une ` QHash ` indexée par l’ operationId(). Le retour de ` Continuation::Proceed ` transmet l’appel plus bas dans la chaîne.
void LoggingInterceptor::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);
const auto codeStr = QDebug::toString(status.code()).section("::", -1);
auto msg = u"Finished in %1 ms. StatusCode: %2"_s.arg(duration).arg(codeStr);
if (!status.message().isEmpty())
msg += u", Message: "_s + status.message();
const auto level = [&] {
switch (status.code()) {
case QtGrpc::StatusCode::Ok:
return LogModel::Level::Info;
case QtGrpc::StatusCode::NotFound:
case QtGrpc::StatusCode::Unauthenticated:
return LogModel::Level::Warning;
default:
return LogModel::Level::Error;
}
}();
m_log->add(level, context.operationId(), context.descriptor(), msg);
}Dans ` onFinished`, l’horodatage stocké est recherché à l’aide de l’ID d’opération afin de calculer la durée du RPC. Le niveau de journalisation est dérivé du code d’état, et l’entrée finale est ajoutée à ` LogModel`.
Les entrées de journal cumulées sont exposées à QML via la propriété ChatEngine::logModel. Un LogDialog dédié les affiche dans un ListView:
Dialog {
id: root
title: "Interceptor Logs"
...
ListView {
ScrollIndicator.horizontal: ScrollIndicator { }
ScrollIndicator.vertical: ScrollIndicator { }
anchors.fill: parent
model: ChatEngine.logModel
clip: true
delegate: ItemDelegate {
id: delegate
required property int level
required property string timestamp
required property int operationId
required property string service
required property string method
required property int rpcType
required property string message
width: ListView.view.width
contentItem: ColumnLayout {
RowLayout {
Label {
font.bold: true
text: root.levelToString(delegate.level)
color: root.levelToColor(delegate.level)
}
Item { Layout.fillWidth: true }
Label {
text: delegate.timestamp
opacity: 0.7
}
}
// further entries for visualizing the RPC context and message.
...
}
}
}
}Chaque délégué affiche le niveau de journalisation, l’horodatage et les détails RPC. L’ LogModel ajoute les nouvelles entrées au début de la liste, de sorte que l’activité la plus récente apparaisse en haut.

Fichiers source
Voir aussi Tous les exemples Qt.
© 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.