Sur cette page

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.

Fenêtre de discussion sur mobile Fenêtre de connexion

Exécution de l’exemple

  • Assurez-vous que le service qtgrpc_chat_server est 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'localhost devrait 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 🚀.

Fenêtre « Paramètres de connexion » (image)

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.

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’ Credentials fournie. 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’ ChatMessage s 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::CallbackService

Vous 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*/) override

Redéfinissez également les fonctions virtuelles afin d’implémenter les fonctionnalités des deux méthodes gRPC fournies par le service :

  • La méthode Register vérifie et stocke les utilisateurs dans une base de données en texte clair.
  • La méthode ChatRoom vé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'QAbstractListModel personnalisé chargé de gérer et de stocker les sessions d'ChatMessage.
  • UserStatusModel: Une QAbstractListModel personnalisé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.Proto

Les 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 singleton ChatEngine.
  • 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.

Boîte de dialogue « Interceptor Logs » contenant les entrées relatives à l'activité RPC

Fichiers source

Exemple de projet sur code.qt.io

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.