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:
- Kommunikation über langlebige QGrpcBidiStreams.
- Verwendung des Clients QtGrpc von einem Worker thread aus.
- Verwendung des Moduls QtProtobufQtCoreTypes im protobuf-Schema.
- Sichere Kommunikation durch SSL.
- Visualisierung von QtProtobuf Nachrichten in einer QML ListView.
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 bereitgestelltenCredentials
. Der Server speichert und verifiziert Benutzer aus einer Datenbank im Klartext.ChatRoom
: Stellt einen bidirektionalen Stream zum Austausch vonChatMessage
(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 ChatMessage
s.
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 benutzerdefiniertesQAbstractListModel
für die Verarbeitung und Speicherung vonChatMessage
s.UserStatusModel
: Ein benutzerdefiniertesQAbstractListModel
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 dasChatEngine
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 vonqtgrpc_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.
© 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.