En esta página

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.

Ventana de chat móvil Ventana de inicio de sesión

Ejecución del ejemplo

  • Asegúrese de que qtgrpc_chat_server está funcionando y escuchando correctamente.
  • Si estás en la misma máquina que el servidor, la dirección localhost por defecto debería ser suficiente al ejecutar qtgrpc_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_FONT de CMake está activada en el cliente para construir con una experiencia emoji fluida 🚀.

Ventana de configuración de la conexión img

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.

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 el Credentials proporcionado . El servidor almacena y verifica los usuarios desde una base de datos en texto plano.
  • ChatRoom: Establece un flujo bidireccional para intercambiar ChatMessage(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 Register verifica y almacena los usuarios en una base de datos de texto plano.
  • El método ChatRoom compara 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: Un QAbstractListModel personalizado para gestionar y almacenar ChatMessages.
  • UserStatusModel: Un QAbstractListModel personalizado 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 singleton ChatEngine.
  • 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

Proyecto de ejemplo @ code.qt.io

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.