QtGrpc Chat

Eine Chat-Anwendung zum Austausch von Nachrichten jeglicher Art in einem Chatroom.

Das Chat-Beispiel demonstriert die erweiterte Nutzung der QtGrpc Client-API. Der Server ermöglicht es Benutzern, sich zu registrieren und zu authentifizieren, so dass sie dem ChatRoom beitreten können. Sobald sie dem ChatRoom beigetreten sind, können die Benutzer verschiedene Nachrichtentypen im ChatRoom austauschen, z. B. Textnachrichten, Bilder, Benutzeraktivitäten oder beliebige andere Dateien von ihrer Festplatte mit allen anderen Teilnehmern.

Einige wichtige Themen, die in diesem Beispiel behandelt werden, sind:

Protobuf-Schema

Das Protobuf-Schema definiert die Struktur der in der Chat-Anwendung verwendeten Nachrichten und Dienste. Das Schema ist in zwei Dateien aufgeteilt:

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

Die Datei qtgrpcchat.proto spezifiziert den Dienst QtGrpcChat, der zwei RPC-Methoden bereitstellt:

  • Register: Registriert einen Benutzer mit der bereitgestellten Credentials. Der Server speichert und verifiziert Benutzer aus einer Datenbank im Klartext.
  • ChatRoom: Stellt einen bidirektionalen Stream zum Austausch von ChatMessage(s) zwischen allen verbundenen Clients her. Der Server sendet alle eingehenden Nachrichten an andere verbundene Clients.
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;
    }
}

Die Datei chatmessages.proto definiert ChatMessage, bei dem es sich um eine getaggte Vereinigung (auch bekannt als Summentyp) handelt. Er repräsentiert alle einzelnen Nachrichten, die über das ChatRoom Streaming RPC gesendet werden können. Jede ChatMessage muss ein username und timestamp enthalten, um den Absender zu identifizieren.

Wir fügen den QtCore/QtCore.proto -Import ein, um die Typen des QtProtobufQtCoreTypes -Moduls zu aktivieren und eine nahtlose Konvertierung zwischen QtCore-spezifischen Typen und ihren Protobuf-Äquivalenten zu ermöglichen.

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 ist einer der unterstützten Nachrichtentypen für den ChatMessage Summentyp. Er ermöglicht es, eine beliebige lokale Datei in eine Nachricht zu verpacken. Das optionale Feld Continuation gewährleistet eine zuverlässige Zustellung, indem es große Dateiübertragungen in Stücken verarbeitet.

Hinweis: Weitere Einzelheiten zur Verwendung des Moduls ProtobufQtCoreTypes in Ihrem Protobuf-Schema und Anwendungscode finden Sie unter Qt Core usage.

Server

Hinweis: Die hier beschriebene Serveranwendung verwendet die gRPC Bibliothek.

Die Serveranwendung verwendet die asynchrone gRPC Callback-API. Dadurch können wir von den Leistungsvorteilen der asynchronen API profitieren, ohne die Komplexität der manuellen Verwaltung von Abschlusswarteschlangen.

class QtGrpcChatService final : public chat::QtGrpcChat::CallbackService

Wir deklarieren die Klasse QtGrpcChatService, die eine Unterklasse von CallbackService des generierten Dienstes QtGrpcChat ist.

    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

Wir überschreiben die virtuellen Funktionen, um die Funktionalität für die beiden vom Dienst bereitgestellten gRPC Methoden zu implementieren:

  • Die Methode Register verifiziert und speichert Benutzer in einer Klartextdatenbank.
  • Die Methode ChatRoom prüft die in den Metadaten angegebenen Anmeldedaten mit der Datenbank. Bei Erfolg wird ein bidirektionaler Stream für die Kommunikation eingerichtet.
    // 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);
        }
    }

Die Dienstimplementierung verfolgt alle aktiven Clients, die sich über die Methode ChatRoom verbinden oder die Verbindung trennen. Dies ermöglicht die Funktion broadcast, die Nachrichten mit allen verbundenen Clients teilt. Um den Speicherbedarf und den Overhead zu verringern, ist die ChatMessage in eine shared_ptr verpackt.

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

Die Methode startSharedWrite ist eine Mitgliedsfunktion von ChatRoomReactor. Wenn der Reaktor (d. h. der Client) gerade schreibt, wird die Nachricht in einer Warteschlange zwischengespeichert. Andernfalls wird ein Schreibvorgang eingeleitet. Es gibt eine einzige und eindeutige Nachricht, die von allen Clients gemeinsam genutzt wird. Jede Kopie der Nachricht response erhöht die use_count. Sobald alle Clients die Nachricht fertig geschrieben haben und die use_count auf 0 sinkt, werden die Ressourcen freigegeben.

    // 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());

Dieses Snippet ist Teil der virtuellen Methode ChatRoomReactor::OnReadDone. Jedes Mal, wenn diese Methode aufgerufen wird, ist eine neue Nachricht von einem Client eingegangen. Die Nachricht wird an alle anderen Clients gesendet, wobei der Absender übersprungen wird.

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

Dieses Snippet ist Teil der virtuellen Methode ChatRoomReactor::OnWriteDone. Jedes Mal, wenn diese Methode aufgerufen wird, wurde eine Nachricht an den Client geschrieben. Wenn in der Warteschlange gepufferte Nachrichten vorhanden sind, wird die nächste Nachricht geschrieben. Andernfalls wird m_response zurückgesetzt, um zu signalisieren, dass kein Schreibvorgang im Gange ist. Eine Sperre wird zum Schutz vor Konflikten mit der Methode broadcast verwendet.

Klient

Die Client-Anwendung verwendet das mitgelieferte Protobuf-Schema zur Kommunikation mit dem Server. Sie bietet sowohl Front-End- als auch Back-End-Funktionen für die Registrierung von Benutzern und den Umgang mit dem langlebigen bidirektionalen Stream der Methode ChatRoom gRPC . Dies ermöglicht die Visualisierung und Kommunikation von ChatMessages.

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

Zunächst generieren wir die Quelldateien aus dem Protobuf-Schema. Da die Datei qtgrpcchat.proto keine message Definitionen enthält, ist nur die Generierung von qtgrpcgen erforderlich. Wir stellen auch die PROTO_INCLUDES des ProtobufQtCoreTypes Moduls zur Verfügung, um sicherzustellen, dass der "QtCore/QtCore.proto" Import gültig ist.

target_link_libraries(qtgrpc_chat_client_proto
    PUBLIC
        Qt6::Protobuf
        Qt6::ProtobufQtCoreTypes
        Qt6::Grpc
)

Wir stellen sicher, dass das unabhängige qtgrpc_chat_client_proto Target öffentlich gegen seine Abhängigkeiten gelinkt wird, einschließlich des ProtobufQtCoreTypes Moduls. Das Anwendungsziel wird dann mit dieser Bibliothek gelinkt.

Backend-Logik

Das Backend der Anwendung besteht aus vier entscheidenden Elementen:

  • ChatEngine: Ein QML-orientiertes Singleton, das die Anwendungslogik verwaltet.
  • ClientWorker: Ein Worker-Objekt, das die Client-Funktionalität gRPC asynchron zur Verfügung stellt.
  • ChatMessageModel: Ein benutzerdefiniertes QAbstractListModel für die Verarbeitung und Speicherung von ChatMessages.
  • UserStatusModel: Ein benutzerdefiniertes QAbstractListModel für die Verwaltung von Benutzeraktivitäten.
    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();

Der obige Ausschnitt zeigt einige der Q_INVOKABLE Funktionen, die von QML aufgerufen werden, um mit dem Server zu interagieren.

    explicit ClientWorker(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);

Die von ClientWorker bereitgestellten Slots spiegeln in gewisser Weise die API von ChatEngine wider. ClientWorker arbeitet in einem eigenen Thread, um kostspielige Vorgänge, wie das Senden oder Empfangen großer Dateien, im Hintergrund zu verarbeiten.

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

Im ChatEngine -Konstruktor weisen wir ClientWorker seinem dedizierten Worker-Thread zu und setzen die Verarbeitung und Weiterleitung seiner Signale fort, um sie auf der QML-Seite verfügbar zu machen.

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(5s));
    const auto *replyPtr = reply.get();
    connect(
        replyPtr, &QGrpcCallReply::finished, this,
        [this, reply = std::move(reply)](const QGrpcStatus &status) {
            emit registerFinished(status);
        },
        Qt::SingleShotConnection);
}

Dies zeigt, wie die ChatEngine mit der ClientWorker interagiert, um Benutzer zu registrieren. Da ClientWorker in seinem eigenen Thread läuft, ist es wichtig, invokeMethod zu verwenden, um seine Mitgliedsfunktionen sicher aufzurufen.

In ClientWorker wird geprüft, ob der Client nicht initialisiert ist oder ob sich der Host-URI geändert hat. Wenn eine der beiden Bedingungen erfüllt ist, rufen wir initializeClient auf, das einen neuen QGrpcHttp2Channel erstellt. Da dies ein kostspieliger Vorgang ist, minimieren wir sein Auftreten.

Um den Register RPC zu behandeln, verwenden wir die Option setDeadlineTimeout, um den Server vor Inaktivität zu schützen. Es wird allgemein empfohlen, für unäre RPCs eine Frist zu setzen.

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

Bei der Anmeldung bei ChatRoom verwenden wir die Option setMetadata, um die vom Server für die Authentifizierung benötigten Benutzerdaten anzugeben. Der eigentliche Aufruf und der Verbindungsaufbau werden in der Methode connectStream abgewickelt.

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

Wir implementieren eine grundlegende Logik zur Wiederherstellung der Verbindung für den Fall, dass der Stream abrupt beendet wird, während wir noch verbunden sind. Dazu wird einfach connectStream erneut mit der QGrpcCallOptions des ersten Aufrufs aufgerufen. Dadurch wird sichergestellt, dass alle erforderlichen Verbindungen auch aktualisiert werden.

Hinweis: Der Doze/App-Standby-Modus von Android kann z. B. durch die Verwendung des FileDialogs oder den Wechsel zu einer anderen App ausgelöst werden. Dieser Modus schaltet den Netzwerkzugang ab, schließt alle aktiven QTcpSocket Verbindungen und führt dazu, dass der Stream auf finished steht. Wir lösen dieses Problem mit der Logik der Wiederverbindung.

    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: // Laden Sie alle Dateinachrichten herunter und speichern Sie die heruntergeladene URL im // Inhalt, so dass das Modell von dort darauf verweisen kann.
            m_chatResponse.file() . setContent(saveFileRequest(m_chatResponse.file()).toString().toUtf8()); break; ... emit chatStreamMessageReceived(m_chatResponse); }); setState(Backend::ChatState::Connecting); }

Wenn Nachrichten empfangen werden, führt ClientWorker einige Vorverarbeitungen durch, wie z. B. das Speichern des Inhalts von FileMessage, so dass sich ChatEngine nur auf die Modelle konzentrieren muss. Wir verwenden das ContentFields enum, um das oneof content Feld unseres ChatMessage-Summentyps sicher zu überprüfen.

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

Beim Senden von Nachrichten erstellt ChatEngine ordnungsgemäß formatierte Anfragen. Die Methode sendText akzeptiert beispielsweise eine QString und verwendet die Funktion createMessage, um eine gültige Nachricht mit den Feldern username und timestamp zu erzeugen. Der Client wird dann aufgerufen, um die Nachricht zu senden, und eine Kopie wird in unsere eigene ChatMessageModel eingereiht.

QML-Frontend
import QtGrpc
import QtGrpcChat
import QtGrpcChat.Proto

Die folgenden Importe werden im QML-Code verwendet:

  • QtGrpc: Bietet QtGrpc QML-Funktionalität, wie z. B. StatusCode.
  • QtGrpcChat: Unser Anwendungsmodul, das Komponenten wie das ChatEngine Singleton enthält.
  • QtGrpcChat.Proto: Bietet QML-Zugriff auf unsere generierten Protobuf-Typen.
    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()
        }
    }

In Main.qml behandeln wir Kernsignale, die von ChatEngine ausgegeben werden. Die meisten dieser Signale werden global behandelt und in jedem Zustand der Anwendung angezeigt.

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

Die generierten Nachrichtentypen aus dem Protobuf-Schema sind in QML zugänglich, da sie QML_VALUE_TYPEsind (eine camelCase-Version der Nachrichten-Definition). Die LoginView.qml verwendet die credentials Werttyp-Eigenschaft, um die login auf der ChatEngine zu initiieren.

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

In ChatView.qml zeigt ListView Nachrichten im ChatRoom an. Dies ist etwas komplexer, da wir den ChatMessage Summentyp bedingt behandeln müssen.

Dazu verwenden wir einen DelegateChooser, der es uns ermöglicht, den geeigneten Delegaten auf der Grundlage des Nachrichtentyps auszuwählen. Wir verwenden die Standardrolle whatThis im Modell, die den Nachrichtentyp für jede ChatMessage -Instanz bereitstellt. Die Komponente DelegateBase greift dann auf die Rolle display des Modells zu und macht die chatMessage-Daten für das Rendering verfügbar.

TextEdit {
    id: root

    required property textMessage message

    text: message.content
    color: "#f3f3f3"
    font.pointSize: 14
    wrapMode: TextEdit.Wrap
    readOnly: true
    selectByMouse: true
}

Hier ist eine der Komponenten, die den Typ TextMessage visualisiert. Sie verwendet den Werttyp textMessage aus dem protobuf-Modul, um den Text zu visualisieren.

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

Der Chat-Client bietet verschiedene Zugangspunkte für das Senden von Nachrichten wie:

  • Akzeptieren von Dateien, die in der Anwendung abgelegt werden.
  • <Strg + V>, um alles zu senden, was in der QZwischenablage gespeichert ist.
  • <Strg + Enter>, um die Nachricht aus der Anwendung zu senden. inputField
  • Klicken auf die Schaltfläche "Senden" für die inputField
  • Auswählen von Dateien über einen FileDialog

SSL

Um die Kommunikation zwischen dem Server und den Clients zu sichern, wird die SSL/TLS-Verschlüsselung verwendet. Dies erfordert mindestens die folgenden Voraussetzungen:

  • Privater Schlüssel: Er enthält den privaten Schlüssel des Servers, der zum Aufbau sicherer Verbindungen verwendet wird. Er muss vertraulich behandelt werden und sollte niemals weitergegeben werden.
  • Zertifikat: enthält das öffentliche Zertifikat des Servers, das an Clients weitergegeben wird, um die Identität des Servers zu überprüfen. Es wird in der Regel von einer Zertifizierungsstelle (CA) signiert oder kann zu Testzwecken selbst signiert werden.
  • Optionales Root-CA-Zertifikat: Wenn Sie eine benutzerdefinierte Zertifizierungsstelle (CA) verwenden, um Ihr Serverzertifikat zu signieren, ist das Root-CA-Zertifikat auf der Client-Seite erforderlich, um die Zertifikatskette des Servers zu validieren. Dadurch wird sichergestellt, dass der Client dem Serverzertifikat vertrauen kann, da das Stammzertifikat der benutzerdefinierten CA nicht im Vertrauensspeicher des Clients vorinstalliert ist, wie dies bei öffentlichen CAs der Fall ist.

Wir haben OpenSSL verwendet, um diese Dateien zu erstellen und unsere gRPC -Kommunikation für die Verwendung von SSL/TLS einzurichten.

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

Wir übermitteln den privaten Schlüssel und das Zertifikat an den gRPC Server. Damit bauen wir die SslServerCredentials auf, um TLS auf der Server-Seite zu aktivieren. Zusätzlich zur sicheren Kommunikation erlauben wir auch den unverschlüsselten Zugriff.

Der Server lauscht auf den folgenden Adressen:

  • HTTPS : 0.0.0.0:65002
  • HTTP : 0.0.0.0:65003

Der Server bindet sich an 0.0.0.0, um alle Netzwerkschnittstellen abzuhören, so dass der Zugriff von jedem Gerät im selben Netzwerk möglich ist.

if (m_hostUri.scheme() == "https") { if (!QSslSocket::supportsSsl()) { emit chatError(tr("Das Gerät unterstützt kein SSL. Bitte verwenden Sie das 'http' Schema.")); 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" }); // HTTP/2 zulassen // Hostnamenverifizierung deaktivieren, um Verbindungen von jeder lokalen IP zuzulassen. // Für die Entwicklung akzeptabel, aber aus Sicherheitsgründen in der Produktion vermeiden.sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); opts.setSslConfiguration(sslConfig); }

Der Client lädt das Root-CA-Zertifikat, da wir die CA selbst signiert haben. Dieses Zertifikat wird verwendet, um das QSslCertificate zu erstellen. Es ist wichtig, das "h2" Protokoll mit setAllowedNextProtocols bereitzustellen, da wir HTTP/2 verwenden.

Ausführen des Beispiels

  • Vergewissern Sie sich, dass qtgrpc_chat_server läuft und erfolgreich lauscht.
  • Wenn Sie sich auf demselben Rechner wie der Server befinden, sollte die Standardadresse localhost für die Ausführung von qtgrpc_chat_client ausreichen. Wenn Sie ein anderes Gerät als das verwenden, auf dem der Server läuft, geben Sie im Dialogfeld Einstellungen die korrekte IP-Adresse des Hosts an, auf dem der Server läuft.
  • Vergewissern Sie sich, dass die GRPC_CHAT_USE_EMOJI_FONT CMake-Option auf dem Client aktiviert ist, um eine reibungslose Emoji-Erfahrung zu ermöglichen 🚀.

Zum Ausführen des Beispiels von Qt Creatorzu starten, öffnen Sie den Modus Welcome und wählen Sie das Beispiel von Examples aus. Für weitere Informationen siehe Qt Creator: Tutorial: Erstellen und Ausführen.

Beispielprojekt @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.