Chat QtGrpc
Una aplicación de chat para compartir mensajes de cualquier tipo en una sala de chat.
El ejemplo Chat demuestra el uso avanzado de la API de cliente QtGrpc. El servidor permite a los usuarios registrarse y autenticarse, permitiéndoles unirse a la ChatRoom. Una vez unidos, los usuarios pueden compartir varios tipos de mensajes en la ChatRoom, como mensajes de texto, imágenes, actividad del usuario o cualquier otro archivo de su disco con el resto de participantes.

Ejecución del ejemplo
- Asegúrese de que
qtgrpc_chat_serverestá funcionando y escuchando correctamente. - Si estás en la misma máquina que el servidor, la dirección
localhostpor defecto debería ser suficiente al ejecutarqtgrpc_chat_client. Si estás utilizando un dispositivo distinto al que aloja el servidor, especifica la dirección IP correcta del host que ejecuta el servidor en el cuadro de diálogo Configuración. - Asegúrate de que la opción
GRPC_CHAT_USE_EMOJI_FONTde CMake está activada en el cliente para construir con una experiencia emoji fluida 🚀.

Para ejecutar el ejemplo desde Qt Creatorabra el modo Welcome y seleccione el ejemplo de Examples. Para obtener más información, consulte Qt Creator: Tutorial: Construir y ejecutar.
Módulos y clases relevantes.
Este ejemplo introduce los siguientes módulos y clases de Qt.
- Comunicación a través de QGrpcBidiStream.
- Uso del cliente QtGrpc desde un trabajador thread.
- Uso del módulo QtProtobufQtCoreTypes en el esquema protobuf.
- Comunicación segura a través de SSL.
- Visualización de mensajes QtProtobuf en un QML ListView.
Esquema Protobuf
El esquema Protobuf define la estructura de los mensajes y servicios utilizados en la aplicación de chat. El esquema se divide en dos archivos:
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) {}
}El archivo qtgrpcchat.proto especifica el servicio QtGrpcChat, que proporciona dos métodos RPC:
Register: Registra un usuario con elCredentialsproporcionado . El servidor almacena y verifica los usuarios desde una base de datos en texto plano.ChatRoom: Establece un flujo bidireccional para intercambiarChatMessage(s) entre todos los clientes conectados. El servidor difunde todos los mensajes entrantes a otros clientes conectados.
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;
}
}El archivo chatmessages.proto define ChatMessage, que es una unión etiquetada (también conocida como tipo suma). Representa todos los mensajes individuales que pueden enviarse a través del RPC de transmisión ChatRoom. Cada ChatMessage debe incluir un username y timestamp para identificar al remitente.
Se incluye la importación QtCore/QtCore.proto para habilitar los tipos del módulo QtProtobufQtCoreTypes, lo que permite una conversión fluida entre los tipos específicos de QtCore y sus equivalentes en 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 es uno de los tipos de mensaje compatibles con el tipo de suma ChatMessage. Permite envolver cualquier archivo local en un mensaje. El campo opcional Continuation garantiza una entrega fiable gestionando las transferencias de archivos grandes en trozos.
Nota: Para más detalles sobre el uso del módulo ProtobufQtCoreTypes en el esquema de Protobuf y el código de aplicación, véase Qt Core usage.
Servidor
Nota: La aplicación de servidor descrita aquí utiliza la biblioteca gRPC™ biblioteca.
La aplicación de servidor utiliza la API de devolución de llamada asíncrona gRPC. Esto nos permite beneficiarnos de las ventajas de rendimiento de la API asíncrona sin la complejidad de gestionar manualmente las colas de finalización.
class QtGrpcChatService final : public chat::QtGrpcChat::CallbackService
Se declara la clase QtGrpcChatService, que subclasea la CallbackService del servicio QtGrpcChat generado.
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
También anula las funciones virtuales para implementar la funcionalidad de los dos métodos gRPC proporcionados por el servicio:
- El método
Registerverifica y almacena los usuarios en una base de datos de texto plano. - El método
ChatRoomcompara las credenciales proporcionadas en los metadatos con la base de datos. Si tiene éxito, establece un flujo bidireccional para la comunicación.
// 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); } }
La implementación del servicio realiza un seguimiento de todos los clientes activos que se conectan o desconectan a través del método ChatRoom. Esto permite la funcionalidad broadcast, que comparte mensajes con todos los clientes conectados. Para reducir el almacenamiento y la sobrecarga, ChatMessage se envuelve en 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()); } }
El método startSharedWrite es una función miembro de ChatRoomReactor. Si el reactor está escribiendo en ese momento, el mensaje se almacena en una cola. En caso contrario, se inicia una operación de escritura. Existe un único mensaje compartido entre todos los clientes. Cada copia del mensaje response incrementa el use_count. Una vez que todos los clientes han terminado de escribir el mensaje, y su use_count cae a 0 se liberan sus recursos.
// 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());
Este fragmento forma parte del método virtual ChatRoomReactor::OnReadDone. Cada vez que se llama a este método, se ha recibido un nuevo mensaje del cliente. El mensaje se difunde a todos los demás clientes, saltándose al remitente.
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();
Este fragmento forma parte del método virtual ChatRoomReactor::OnWriteDone. Cada vez que se llama a este método, se ha escrito un mensaje en el cliente. Si hay mensajes almacenados en la cola, se escribe el siguiente mensaje. En caso contrario, m_response se reinicia para indicar que no hay ninguna operación de escritura en curso. Se utiliza un bloqueo para proteger contra la contención con el método broadcast.
Cliente
La aplicación cliente utiliza el esquema Protobuf proporcionado para comunicarse con el servidor. Proporciona capacidades front-end y back-end para registrar usuarios y manejar el flujo bidireccional de larga duración del método ChatRoom gRPC . Esto permite la visualización y comunicación de ChatMessages.
Configuración
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>
)En primer lugar, genere los archivos fuente a partir del esquema Protobuf. Dado que el archivo qtgrpcchat.proto no contiene definiciones de message, sólo es necesario generar qtgrpcgen. Proporcione también el PROTO_INCLUDES del módulo ProtobufQtCoreTypes para asegurarse de que la importación "QtCore/QtCore.proto" es válida.
target_link_libraries(qtgrpc_chat_client_proto
PUBLIC
Qt6::Protobuf
Qt6::ProtobufQtCoreTypes
Qt6::Grpc
)Asegúrese de que el objetivo independiente qtgrpc_chat_client_proto está vinculado públicamente con sus dependencias, incluido el módulo ProtobufQtCoreTypes. A continuación, el objetivo de la aplicación se enlaza con esta biblioteca.
Lógica de backend
El backend de la aplicación se construye alrededor de cuatro elementos cruciales:
ChatEngine: Un singleton QML que gestiona la lógica de la aplicación.ClientWorker: Un objeto trabajador que proporciona funcionalidad de cliente gRPC de forma asíncrona.ChatMessageModel: UnQAbstractListModelpersonalizado para gestionar y almacenarChatMessages.UserStatusModel: UnQAbstractListModelpersonalizado para gestionar la actividad de los usuarios.
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();
El fragmento anterior muestra parte de la funcionalidad Q_INVOKABLE que se llama desde QML para interactuar con el servidor.
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);
Las ranuras proporcionadas por ClientWorker reflejan en cierto modo la API expuesta por ChatEngine. ClientWorker funciona en un subproceso dedicado para gestionar operaciones costosas, como la transmisión o recepción de archivos de gran tamaño, en segundo plano.
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); ...
En el constructor de ChatEngine, asigna ClientWorker a su subproceso trabajador dedicado y sigue gestionando y reenviando sus señales para que estén disponibles en el lado 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(5s)); const auto *replyPtr = reply.get(); connect( replyPtr, &QGrpcCallReply::finished, this, [this, reply = std::move(reply)](const QGrpcStatus &status) { emit registerFinished(status); }, Qt::SingleShotConnection); }
Esto demuestra cómo ChatEngine interactúa con ClientWorker para registrar usuarios. Dado que ClientWorker se ejecuta en su propio subproceso, es importante utilizar invokeMethod para llamar a sus funciones miembro de forma segura.
En ClientWorker, se comprueba si el cliente no está inicializado o si el URI del host ha cambiado. Si se cumple cualquiera de las dos condiciones, llama a initializeClient, que crea un nuevo QGrpcHttp2Channel. Dado que esta es una operación costosa, minimiza sus ocurrencias.
Para gestionar la RPC Register, utilice la opción setDeadlineTimeout para protegerse de la inactividad del servidor. Generalmente se recomienda establecer una fecha límite para las RPC unarias.
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); }
Al iniciar sesión en ChatRoom,puede utilizar la opción setMetadata para proporcionar las credenciales de usuario, según lo requiera el servidor para la autenticación. La llamada real y la configuración de la conexión se manejan en el método 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); } }); ...
Para implementar la lógica básica de reconexión en caso de que el flujo termine abruptamente mientras sigues conectado. Esto se hace simplemente llamando de nuevo a connectStream con el QGrpcCallOptions de la llamada inicial. Esto asegura que todas las conexiones requeridas también sean actualizadas.
Nota: El modo Doze/App-Standby de Android puede activarse, por ejemplo, utilizando el FileDialog o cambiando a otra app. Este modo cierra el acceso a la red, cerrando todas las conexiones activas de QTcpSocket y provocando que el stream sea finished. Puedes solucionar este problema con la lógica de reconexión.
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: // Descarga cualquier mensaje de archivo y almacena la URL descargada en el // contenido, permitiendo que el modelo haga referencia a él desde allí. m_chatResponse.file() . setContent(saveFileRequest(m_chatResponse.file()).toString().toUtf8()); break; ... emite chatStreamMessageReceived(m_chatResponse); }); setState(Backend::ChatState::Connecting); }
Cuando se reciben los mensajes, ClientWorker realiza algún preprocesamiento, como guardar el contenido de FileMessage, para que ChatEngine sólo tenga que centrarse en los modelos. Utiliza el enum ContentFields para comprobar de forma segura el campo oneof content de nuestro tipo de suma 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); }
Al enviar mensajes, el ChatEngine crea peticiones con el formato adecuado. Por ejemplo, el método sendText acepta un QString y utiliza la función createMessage para generar un mensaje válido con los campos username y timestamp establecidos. El cliente es entonces invocado para enviar el mensaje, y una copia es puesta en cola en nuestro propio ChatMessageModel.
Interfaz QML
import QtGrpc import QtGrpcChat import QtGrpcChat.Proto
En el código QML se utilizan las siguientes importaciones:
QtGrpc: Proporciona QtGrpc funcionalidad QML, como el StatusCode.QtGrpcChat: Nuestro módulo de aplicación, que incluye componentes como el singletonChatEngine.QtGrpcChat.Proto: Proporciona acceso QML a nuestros tipos protobuf generados.
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 maneja las señales centrales emitidas por el ChatEngine. La mayoría de estas señales se manejan globalmente y se visualizan en cualquier estado de la aplicación.
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) } }
Los tipos de mensaje generados a partir del esquema protobuf son accesibles en QML ya que son QML_VALUE_TYPEs (una versión camelCase de la definición del mensaje). El LoginView.qml utiliza la propiedad de tipo de valor credentials para iniciar el login en el 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 } } }
En ChatView.qml, el ListView muestra los mensajes en el ChatRoom. Esto es ligeramente más complejo, ya que es necesario manejar el tipo de suma ChatMessage condicionalmente.
Puedes utilizar un DelegateChooser, que nos permite seleccionar el delegado apropiado en función del tipo de mensaje. Utilice el rol por defecto whatThis en el modelo, que proporciona el tipo de mensaje para cada instancia ChatMessage. El componente DelegateBase accede entonces al rol display del modelo, haciendo que los datos de chatMessage estén disponibles para ser renderizados.
TextEdit { id: root required property textMessage message text: message.content color: "#f3f3f3" font.pointSize: 14 wrapMode: TextEdit.Wrap readOnly: true selectByMouse: true }
Aquí se muestra uno de los componentes que visualiza el tipo TextMessage. Utiliza el tipo de valor textMessage del módulo protobuf para visualizar el texto.
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 } }
El cliente de Chat proporciona varios puntos de acceso en el envío de mensajes como:
- Aceptar archivos soltados en la aplicación.
- <Ctrl + V> para enviar cualquier cosa almacenada en el QClipboard.
- <Ctrl + Enter> para enviar el mensaje desde la aplicación.
inputField - Hacer clic en el botón de envío para
inputField - Selección de archivos a través de un FileDialog
SSL
Para asegurar la comunicación entre el servidor y los clientes, se utiliza el cifrado SSL/TLS. Esto requiere como mínimo lo siguiente
- Clave privada: contiene la clave privada del servidor, que se utiliza para establecer conexiones seguras. Debe mantenerse confidencial y nunca debe compartirse.
- Certificado: contiene el certificado público del servidor, que se comparte con los clientes para verificar la identidad del servidor. Suele estar firmado por una Autoridad de Certificación (CA) o puede ser autofirmado para realizar pruebas.
- Certificado CA raíz opcional: Si utiliza una autoridad de certificación (CA) personalizada para firmar su certificado de servidor, el certificado de CA raíz es necesario en el lado del cliente para validar la cadena de certificados del servidor. Esto garantiza que el cliente pueda confiar en el certificado del servidor, ya que el certificado raíz de la CA personalizada no está preinstalado en el almacén de confianza del cliente como los de las CA públicas.
Utilice OpenSSL para crear estos archivos y configurar nuestra comunicación gRPC para utilizar 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());
Proporcione la clave privada y el certificado al servidor gRPC. Con eso, usted puede construir el SslServerCredentials para habilitar TLS en el lado del servidor. Además de la comunicación segura, también permitir el acceso sin cifrar.
El servidor escucha en las siguientes direcciones:
- HTTPS :
0.0.0.0:65002 - HTTP :
0.0.0.0:65003
El servidor se enlaza a 0.0.0.0 para escuchar en todas las interfaces de red, permitiendo el acceso desde cualquier dispositivo de la misma red.
¡if (m_hostUri.scheme() == "https") { if (!QSslSocket::supportsSsl()) { emit chatError(tr("El dispositivo no soporta SSL. Por favor, utilice el esquema 'http'.")); return false; } QFile crtFile(":/res/root.crt"); if (!crtFile.open(QFile::Sólo lectura)) { 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" }); // Permitir HTTP/2 // Desactivar la verificación del nombre de host para permitir conexiones desde cualquier IP local. // Aceptable para desarrollo pero evitar en producción por seguridad.sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); opts.setSslConfiguration(sslConfig); }
El cliente carga el Certificado de CA Raíz, ya que autofirmó la CA. Este certificado se utiliza para crear el QSslCertificate. Es importante proporcionar el protocolo "h2" con setAllowedNextProtocols, ya que está utilizando HTTP/2.
Archivos fuente
Ver también Todos los ejemplos de 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.