QtGrpc-Chat
Eine Chat-Anwendung zum Austausch beliebiger Nachrichten in einem Chatraum.
Das Chat -Beispiel veranschaulicht die fortgeschrittene Nutzung der Client-API „ Qt GRPC “. Der Server ermöglicht es Benutzern, sich zu registrieren und zu authentifizieren, wodurch sie dem ChatRoom beitreten können. Nach dem Beitritt können Benutzer verschiedene Nachrichtentypen im ChatRoom mit allen anderen Teilnehmern austauschen, beispielsweise Textnachrichten, Bilder, Benutzeraktivitäten oder beliebige andere Dateien von ihrer Festplatte.

Ausführen des Beispiels
- Stellen Sie sicher, dass der „
qtgrpc_chat_server“ läuft und erfolgreich auf Verbindungen wartet. - Wenn Sie sich auf demselben Rechner wie der Server befinden, sollte die Standardadresse des „
localhost“ ausreichen, wenn Sie den Befehl „qtgrpc_chat_client“ ausführen. Wenn Sie ein anderes Gerät als dasjenige verwenden, auf dem der Server gehostet wird, geben Sie im Dialogfeld „Einstellungen“ die korrekte IP-Adresse des Hosts an, auf dem der Server läuft. - Stellen Sie sicher, dass die CMake-Option „
GRPC_CHAT_USE_EMOJI_FONT“ auf dem Client aktiviert ist, um eine reibungslose Emoji-Erfahrung zu gewährleisten 🚀.

Um das Beispiel aus Qt Creator, öffnen Sie den Modus „ Welcome “ und wählen Sie das Beispiel unter Examples aus. Weitere Informationen finden Sie unter Qt Creator: Tutorial: Erstellen und Ausführen.
Relevante Module und Klassen.
Dieses Beispiel stellt die folgenden Qt-Module und -Klassen vor.
- Kommunikation über langlebige „ QGrpcBidiStream “.
- Verwendung des „
QtGrpc“-Clients aus einem Worker- thread. - Verwendung des Moduls „ QtProtobufQtCoreTypes “ im Protobuf-Schema.
- Sichere Kommunikation über SSL.
- Visualisierung von „ QtProtobuf “-Nachrichten in einem 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 unterteilt:
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 dem angegebenen „Credentials“. Der Server speichert und verifiziert Benutzer aus einer Datenbank im Klartext.ChatRoom: Stellt einen bidirektionalen Datenstrom für den Austausch vonChatMessage(s) zwischen allen verbundenen Clients her. Der Server leitet alle eingehenden Nachrichten an die anderen verbundenen Clients weiter.
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 als Summentyp bezeichnet) handelt. Sie repräsentiert alle einzelnen Nachrichten, die über das „ ChatRoom “-Streaming-RPC gesendet werden können. Jede „ ChatMessage “ muss eine „ username “ und eine „ timestamp “ enthalten, um den Absender zu identifizieren.
Sie fügen den Import „ QtCore/QtCore.proto “ ein, um die Typen des Moduls „ QtProtobufQtCoreTypes “ zu aktivieren, was eine nahtlose Konvertierung zwischen den für „ QtCore “ spezifischen Typen und ihren Protobuf-Entsprechungen ermöglicht.
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 Summentyp „ ChatMessage “. Er ermöglicht es, beliebige lokale Dateien in eine Nachricht einzubinden. Das optionale Feld „ Continuation “ gewährleistet eine zuverlässige Zustellung, indem es die Übertragung großer Dateien in Blöcken abwickelt.
Hinweis: Weitere Informationen 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 nutzt die asynchrone Callback-API „ gRPC “. Dadurch können wir von den Leistungsvorteilen der asynchronen API profitieren, ohne die Komplexität der manuellen Verwaltung von Abschlusswarteschlangen in Kauf nehmen zu müssen.
class QtGrpcChatService final : public chat::QtGrpcChat::CallbackServiceSie deklarieren die Klasse ` QtGrpcChatService `, die eine Unterklasse der Klasse ` CallbackService ` des generierten Dienstes ` QtGrpcChat ` darstellt.
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Überschreiben Sie außerdem die virtuellen Funktionen, um die Funktionalität für die beiden vom Dienst bereitgestellten „ gRPC “-Methoden zu implementieren:
- Die Methode „
Register“ überprüft die Anmeldedaten und speichert die Benutzer in einer Klartext-Datenbank. - Die Methode „
ChatRoom“ gleicht die in den Metadaten angegebenen Anmeldedaten mit der Datenbank ab. Bei Erfolg wird ein bidirektionaler Kommunikationsstrom aufgebaut.
// 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 Service-Implementierung verfolgt alle aktiven Clients, die über die Methode „ ChatRoom “ eine Verbindung herstellen oder trennen. Dies ermöglicht die „ broadcast “-Funktionalität, die Nachrichten an alle verbundenen Clients weiterleitet. Um Speicherplatz und Overhead zu reduzieren, wird die „ ChatMessage “ in eine „ shared_ptr “ eingebettet.
// 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 Memberfunktion von ` ChatRoomReactor`. Wenn der Reaktor gerade schreibt, wird die Nachricht in einer Warteschlange gepuffert. Andernfalls wird ein Schreibvorgang initiiert. Es gibt eine einzige, einheitliche Nachricht, die von allen Clients gemeinsam genutzt wird. Jede Kopie der ` response `-Nachricht erhöht den Wert von ` use_count`. Sobald alle Clients das Schreiben der Nachricht abgeschlossen haben und der Wert von ` 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());Dieser Codeausschnitt ist Teil der virtuellen Methode „ ChatRoomReactor::OnReadDone “. Bei jedem Aufruf dieser Methode wurde eine neue Nachricht vom Client empfangen. Die Nachricht wird an alle anderen Clients weitergeleitet, 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();Dieser Codeausschnitt ist Teil der virtuellen Methode „ ChatRoomReactor::OnWriteDone “. Bei jedem Aufruf dieser Methode wurde eine Nachricht an den Client geschrieben. Befinden sich gepufferte Nachrichten in der Warteschlange, wird die nächste Nachricht geschrieben. Andernfalls wird „ m_response “ zurückgesetzt, um zu signalisieren, dass kein Schreibvorgang läuft. Eine Sperre dient dem Schutz vor Konflikten mit der Methode „ broadcast “.
Client
Die Client-Anwendung nutzt das bereitgestellte Protobuf-Schema zur Kommunikation mit dem Server. Sie bietet sowohl Frontend- als auch Backend-Funktionen zur Registrierung von Benutzern und zur Verarbeitung des langlebigen bidirektionalen Datenstroms der Methode „ ChatRoom “ ( gRPC ). Dies ermöglicht die Visualisierung und Kommunikation von „ ChatMessage“.
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>
)Generieren Sie zunächst die Quelldateien aus dem Protobuf-Schema. Da die Datei „ qtgrpcchat.proto “ keine „ message “-Definitionen enthält, ist lediglich die Generierung mit „qtgrpcgen“ erforderlich. Stellen Sie außerdem die „ PROTO_INCLUDES “ des Moduls „ ProtobufQtCoreTypes “ bereit, um sicherzustellen, dass der Import von „ "QtCore/QtCore.proto" “ gültig ist.
target_link_libraries(qtgrpc_chat_client_proto
PUBLIC
Qt6::Protobuf
Qt6::ProtobufQtCoreTypes
Qt6::Grpc
)Stellen Sie sicher, dass das eigenständige Ziel „ qtgrpc_chat_client_proto “ öffentlich mit seinen Abhängigkeiten verknüpft ist, einschließlich des Moduls „ ProtobufQtCoreTypes “. Das Anwendungsziel wird dann mit dieser Bibliothek verknüpft.
Backend-Logik
Das Backend der Anwendung basiert auf vier wesentlichen Elementen:
ChatEngine: Ein QML-orientiertes Singleton, das die Anwendungslogik verwaltet.ClientWorker: Ein Worker-Objekt, das die Client-Funktionalität von „ gRPC “ asynchron bereitstellt.ChatMessageModel: Ein benutzerdefiniertes „QAbstractListModel“ zur Verarbeitung und Speicherung von „ChatMessages“.UserStatusModel: Ein benutzerdefiniertes „QAbstractListModel“ zur Verwaltung der 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 Codeausschnitt zeigt einige der „ Q_INVOKABLE “-Funktionen, die von QML aus aufgerufen werden, um mit dem Server zu interagieren.
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);Die von der „ ClientWorker “ bereitgestellten Slots spiegeln in gewisser Weise die API wider, die von der „ ChatEngine “ bereitgestellt wird. Die „ ClientWorker “ läuft in einem eigenen Thread, um rechenintensive Vorgänge wie das Senden oder Empfangen großer Dateien im Hintergrund abzuwickeln.
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);
...Weisen Sie im Konstruktor von ChatEngine den ClientWorker seinem dedizierten Worker-Thread zu und fahren Sie fort, dessen Signale zu verarbeiten und weiterzuleiten, 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(2s));
const auto *replyPtr = reply.get();
connect(
replyPtr, &QGrpcCallReply::finished, this,
[this, reply = std::move(reply)](const QGrpcStatus &status) {
emit registerFinished(status);
},
Qt::SingleShotConnection);
}Dies veranschaulicht, wie der ` ChatEngine ` mit dem ` ClientWorker ` interagiert, um Benutzer zu registrieren. Da der ` ClientWorker ` in einem eigenen Thread läuft, ist es wichtig, ` invokeMethod ` zu verwenden, um seine Memberfunktionen sicher aufzurufen.
Im ` ClientWorker` prüfen Sie, ob der Client noch nicht initialisiert ist oder ob sich die Host-URI geändert hat. Ist eine der beiden Bedingungen erfüllt, rufen Sie ` initializeClient` auf, wodurch ein neuer ` QGrpcHttp2Channel` erstellt wird. Da dies ein ressourcenintensiver Vorgang ist, sollten Sie dessen Häufigkeit so gering wie möglich halten.
Verwenden Sie zur Abwicklung des RPCs „ Register “ die Option „ setDeadlineTimeout “, um sich vor Serverinaktivität zu schützen. Es wird generell empfohlen, für unäre RPCs eine Frist festzulegen.
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 der „ ChatRoom “ können Sie die Option „ setMetadata “ verwenden, um die vom Server für die Authentifizierung benötigten Benutzeranmeldedaten 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);
}
});
...Um eine grundlegende Logik zur Wiederherstellung der Verbindung zu implementieren, falls der Stream abrupt endet, während Sie noch verbunden sind. Dies geschieht durch einen einfachen erneuten Aufruf von ` connectStream ` mit dem ` QGrpcCallOptions ` aus dem ursprünglichen Aufruf. Dadurch wird sichergestellt, dass auch alle erforderlichen Verbindungen aktualisiert werden.
Hinweis: Der Doze-/App-Standby-Modusvon Android kann beispielsweise durch die Verwendung des FileDialog oder durch den Wechsel zu einer anderen App ausgelöst werden. Dieser Modus unterbricht den Netzwerkzugriff, schließt alle aktiven „ QTcpSocket “-Verbindungen und führt dazu, dass der Stream den Status „ finished “ annimmt. Sie können dieses Problem mit der Wiederverbindungslogik beheben.
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:
// Alle Dateinachrichten herunterladen und die heruntergeladene URL im
// Inhalt speichern , damit 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 der ` ClientWorker ` einige Vorverarbeitungsschritte durch, wie beispielsweise das Speichern des ` FileMessage `-Inhalts, sodass sich der ` ChatEngine ` ausschließlich auf die Modelle konzentrieren muss. Verwenden Sie die Enumeration ` ContentFields `, um das Feld ` oneof content ` unseres Summentyps `ChatMessage` 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 Versenden von Nachrichten erstellt der „ ChatEngine “ korrekt formatierte Anfragen. Beispielsweise akzeptiert die Methode „ sendText “ einen „ QString “ und verwendet die Funktion „ createMessage “, um eine gültige Nachricht zu generieren, in der die Felder „ username “ und „ timestamp “ gesetzt sind. Anschließend wird der Client aufgerufen, um die Nachricht zu versenden, und eine Kopie wird in unsere eigene Warteschlange „ ChatMessageModel “ gestellt.
QML-Frontend
import QtGrpc
import QtGrpcChat
import QtGrpcChat.ProtoIm QML-Code werden die folgenden Importe verwendet:
QtGrpc: Stellt die QML-Funktionalität von „ QtGrpc “ bereit, wie beispielsweise die „ 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()
}
}Main.qml verarbeitet die von „ ChatEngine “ ausgesendeten Kernsignale. Die meisten dieser Signale werden global verarbeitet und in jedem Zustand der Anwendung visualisiert.
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 aus dem Protobuf-Schema generierten Nachrichtentypen sind in QML zugänglich, da es sich um „ QML_VALUE_TYPEs“ handelt (eine CamelCase -Version der Nachrichtendefinition). Das „ LoginView.qml “ verwendet die Eigenschaft vom Werttyp „ credentials “, um den „ login “ auf dem „ 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 der „ ListView “ Meldungen im „ ChatRoom “ an. Dies ist etwas komplexer, da Sie den Summentyp „ ChatMessage “ bedingt verarbeiten müssen.
Sie können einen „DelegateChooser“ verwenden, der es uns ermöglicht, den passenden Delegaten basierend auf dem Nachrichtentyp auszuwählen. Verwenden Sie 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 stellt die „chatMessage“-Daten für die Darstellung bereit.
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 darzustellen.
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 Zugriffsmöglichkeiten zum Senden von Nachrichten, wie zum Beispiel:
- Das Akzeptieren von Dateien, die auf die Anwendung gezogen werden.
- <Strg + V>, um den Inhalt der QClipboard zu senden.
- <Strg + Eingabetaste>, um die Nachricht über die
inputField - Klicken auf die Senden-Schaltfläche für die
inputField - Auswahl von Dateien über einen Dateidialog
SSL
Zur Sicherung der Kommunikation zwischen Server und Clients wird eine SSL/TLS-Verschlüsselung verwendet. Dazu sind mindestens folgende Voraussetzungen erforderlich:
- Privater Schlüssel: Enthält den privaten Schlüssel des Servers, der zum Aufbau sicherer Verbindungen verwendet wird. Er muss vertraulich behandelt werden und darf niemals weitergegeben werden.
- Zertifikat: Enthält das öffentliche Zertifikat des Servers, das an die 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 sein.
- Optionales Stamm-CA-Zertifikat: Wenn Sie eine benutzerdefinierte Zertifizierungsstelle (CA) zum Signieren Ihres Serverzertifikats verwenden, wird auf der Client-Seite das Stamm-CA-Zertifikat benötigt, um die Zertifikatskette des Servers zu validieren. Dadurch wird sichergestellt, dass der Client dem Zertifikat des Servers vertrauen kann, da das Stammzertifikat der benutzerdefinierten CA nicht wie die Zertifikate öffentlicher Zertifizierungsstellen im Vertrauensspeicher des Clients vorinstalliert ist.
Verwenden Sie OpenSSL, um diese Dateien zu erstellen und unsere „ gRPC “-Kommunikation für die Nutzung 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());Sie stellen dem gRPC -Server den privaten Schlüssel und das Zertifikat zur Verfügung. Damit können Sie die Datei „ SslServerCredentials “ erstellen, um TLS auf Serverseite zu aktivieren. Erlauben Sie neben der sicheren Kommunikation auch den unverschlüsselten Zugriff.
Der Server hört auf den folgenden Adressen:
- HTTPS:
0.0.0.0:65002 - HTTP:
0.0.0.0:65003
Der Server ist an 0.0.0.0 gebunden, um auf allen Netzwerkschnittstellen zu lauschen, wodurch 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 verwende 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
// Hostnamenüberprüfung deaktivieren, um Verbindungen von beliebigen lokalen IP-Adressen zuzulassen.
// In der Entwicklung akzeptabel, in der Produktion jedoch aus Sicherheitsgründen zu vermeiden.
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
opts.setSslConfiguration(sslConfig);
}Der Client lädt das Root-CA-Zertifikat, da Sie die CA selbst signiert haben. Dieses Zertifikat wird zur Erstellung der ` QSslCertificate` verwendet. Es ist wichtig, das ` "h2" `-Protokoll mit ` setAllowedNextProtocols` anzugeben, da Sie HTTP/2 verwenden.
Interceptoren
Der Chat-Client nutzt eine „ QGrpcInterceptorChain “, um alle RPC-Aktivitäten zu überwachen. Während der Initialisierung wird ein „ LoggingInterceptor “ an den Kanal angehängt, wodurch die Anwendung einen Echtzeit-Überblick über jeden Vorgang erhält, der die Kette durchläuft.
QGrpcInterceptorChain interceptorChain;
interceptorChain.add(std::make_unique<LoggingInterceptor>(m_logModel));
auto channel = std::make_shared<QGrpcHttp2Channel>(m_hostUri, opts,
std::move(interceptorChain));Sie erstellen einen „ QGrpcInterceptorChain “ und fügen den Interceptor mithilfe seiner Überladung „ unique_ptr “ hinzu, wodurch das Eigentumsrecht an die Kette übertragen wird. Anschließend übergeben Sie die Kette an den Konstruktor von „ QGrpcHttp2Channel “. Nachdem der Kanal das Eigentumsrecht übernommen hat, bleibt der Interceptor für die gesamte Lebensdauer des Kanals gültig.
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 implementiert alle verfügbaren Interceptor-Schnittstellen und deckt damit den gesamten RPC-Lebenszyklus ab. Er enthält ein ` shared_ptr ` zu einem ` LogModel ` zum Hinzufügen von Protokolleinträgen sowie ein ` QHash ` zur Verfolgung aktiver Operationen anhand ihrer ID.
Implementierung des Protokollierungs-Interceptors
Die folgenden Codeausschnitte zeigen, wie die Interception-Punkte „ onStart “ und „ onFinished “ funktionieren. Die übrigen Interception-Punkte führen eine grundlegende Protokollierung ihrer jeweiligen Ereignisse durch.
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;
}In „ onStart “ speichert der Interceptor den aktuellen Zeitstempel in einem „ QHash “, der mit dem „ operationId “ () indiziert ist. Durch die Rückgabe von „ Continuation::Proceed “ wird der Aufruf in der Kette weitergeleitet.
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);
}In ` onFinished` wird der gespeicherte Zeitstempel anhand der Operations-ID abgerufen, um die RPC-Dauer zu berechnen. Die Protokollierungsstufe wird aus dem Statuscode abgeleitet, und der endgültige Eintrag wird dem ` LogModel` hinzugefügt.
Die gesammelten Protokolleinträge werden über die Eigenschaft ` ChatEngine::logModel ` an QML weitergegeben. Ein spezielles ` LogDialog ` rendert sie in einem ` 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.
...
}
}
}
}Jeder Delegat zeigt die Protokollstufe, den Zeitstempel und die RPC-Details an. Der ` LogModel ` fügt neue Einträge am Anfang hinzu, sodass die jüngste Aktivität ganz oben erscheint.

Quelldateien
Siehe auch Alle Qt-Beispiele.
© 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.