Solicitudes HTTP GET con Qt y en Qml (async)

 C Programming >> Programación C >  >> Tags >> Qt
Solicitudes HTTP GET con Qt y en Qml (async)

Con Qt es muy fácil trabajar con solicitudes HTTP (asincrónicas). Esta guía le muestra cómo hacerlo con Qt core y en Qml. Los dos ejemplos imprimen el resultado de una solicitud HTTP GET en la pantalla después de presionar un botón. El método Qml usa JavaScript, por lo que es un poco engañoso, el otro método usa C ++ simple con las bibliotecas de Qt para redes (QNetworkAccessManager ) y señales y ranuras para la parte asíncrona.

Esta guía está escrita principalmente porque me encuentro haciendo esto a menudo y sigo buscando en otros proyectos donde ya hice esto para copiar el código. Incluso mis compañeros de trabajo echan un vistazo a mi GitHub para esta cosa específica, me dijeron recientemente, así que mejor ponlo en línea.

Sin usar Qt, probablemente manejaría las solicitudes de red usando curl o algo como cpp-httplib, un cliente/servidor http de solo encabezado. Ya he realizado solicitudes HTTP de red de C++ antiguas y he escrito sobre ello aquí, analizando las API de HackerNews y Lobste.rs.

El código completo de esta guía se puede encontrar en mi github.

Configuración básica

Usando Qt Creator, haz un File , New Project . Seleccione una aplicación de EmptyQt Quick (QML) y finalice el asistente. Estoy usando Qt 5.15, pero el ejemplo también funciona con Qt 6.3.

Este es el main.qml diseño de archivo, 2 filas con un botón y un campo de texto:

Column {
    spacing: 5
    anchors.fill: parent
    anchors.margins: 5

    Row {
        spacing: 5
        Button {
            text: "Qml HTTP GET"
        }

        TextField {
            id: qmlResult
        }
    }

    Row {
        spacing: 5
        Button {
            text: "C++ HTTP GET "
        }

        TextField {
            id: cppResult
        }
    }
}

Solicitud C++ HTTP GET

El antiguo C++ HTTP Get utiliza algunas clases de Qt, a saber, QNetworkAccessManager , QNetworkRequest y QNetworkReply , incluidas algunas señales y ranuras para manejar la solicitud asíncrona.

Comenzaremos haciendo un poco de trabajo, creando la clase derivada de QObject y registrándola para QML Engine. Si ha hecho algo de Qt antes, sabe que lo hará muchas veces y, como yo lo hago, considérelo un trabajo pesado. Cualquier forma de qRegister /qmlRegister lo que necesitas depende de la forma de la luna, pero Qt 6 ha realizado mejoras en ese espectro, ahora usa cmake y solo 1 lugar para registrar objetos.

Crear clases y registro Qml

Haz una nueva clase llamada NetworkExample basado en QObject, ya sea creando los archivos usted mismo o usando Qt Creator Add New asistente, en ese caso seleccione una nueva clase de C++ y asígnele QObject como base:

EjemploDeRed.h

#ifndef NETWORKEXAMPLE_H
#define NETWORKEXAMPLE_H

#include <QObject>

class NetworkExample : public QObject
{
    Q_OBJECT
public:
    explicit NetworkExample(QObject *parent = nullptr);

signals:

};

#endif // NETWORKEXAMPLE_H

EjemploDeRed.cpp

#include "NetworkExample.h"

NetworkExample::NetworkExample(QObject *parent)
    : QObject{parent}
{

}

El archivo no hace nada todavía. En main.cpp , cree una instancia y regístrela en el motor Qml para que podamos importarla en Qml:

#include "NetworkExample.h"
[...] // below the QGuiApplication line
NetworkExample* networkExample = new NetworkExample();
qmlRegisterSingletonInstance<NetworkExample>("org.raymii.NetworkExample", 1, 0, "NetworkExample", networkExample);

En la parte inferior del archivo, cambie el return app.exec() línea para que guardemos ese valor pero también destruyamos nuestro objeto antes de salir:

auto result = app.exec();
networkExample->deleteLater();
return result;

Aunque este es un ejemplo simple, espero enseñarle un poco de higiene agregando explícitamente esta parte.

En main.qml , debajo del otro import líneas:

import org.raymii.NetworkExample 1.0

Solicitud de red

Finalmente, es hora de hacer la solicitud real. Añade el <QNetworkAccessManager> encabezado a sus inclusiones y agregue un QNetworkAccessManager* _manager = nullptr; en el private: sección de su encabezado. Dentro del constructor, new es:

_manager = new QNetworkAccessManager(this);

Dado que proporcionamos un objeto principal, new está bien. Una vez que el padre QObject es destruido, éste también será destruido.

Agregue un método para hacer la solicitud real. En tu encabezado, declara y márcalo como Q_INVOKABLE para que Qml pueda llamarlo:

Q_INVOKABLE void doGetRequest(const QString& url);

La definición de la función:

void NetworkExample::doGetRequest(const QString& url)
{
    setResponse("");
    auto _request = QScopedPointer<QNetworkRequest>(new QNetworkRequest());
    _request->setUrl(url);
    _request->setTransferTimeout(5000);
    _request->setRawHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0");

    QNetworkReply *reply = _manager->get(*_request);
    QObject::connect(reply, &QNetworkReply::finished, this, &NetworkExample::slotFinished);
}

No olvides incluir el <QNetworkReply> encabezado.

La primera parte es un puntero inteligente de estilo Qt, por lo que no tenemos que eliminar ese QNetworkRequest Nosotros mismos. Una vez que sale del alcance, se destruye. La primera línea borra cualquier dato de respuesta anterior en nuestro Q_PROPERTY , lo definiremos más tarde.

A continuación, establecemos algunos parámetros, el más importante es la URL y, como beneficio adicional, he incluido la configuración de un encabezado de agente de usuario y un tiempo de espera de solicitud de 5 segundos.

Usando nuestro QNetworkAccessManager enviamos la solicitud y luego conectamos el finished Señal para responder. Para simplificar esta guía, no voy a conectar el errorOccured o readyRead señales, pero probablemente debería leer los documentos sobre las señales QNetworkReply puede emitir.

Agregue una nueva ranura (método normal, debajo de la línea public slots: ) para nuestro slotFinished método:

public slots:
    void slotFinished();

Contenido:

void NetworkExample::slotFinished()
{
    QNetworkReply *reply = dynamic_cast<QNetworkReply*>(sender());
    if(reply != nullptr) {
        setResponse(reply->readAll());
        reply->deleteLater();
    }
}

Cada signal/slot la conexión tiene un método que devuelve un puntero al objeto que envió la señal, QObject::sender() . Lo estoy usando con un dynamic_cast para asegurarse de que no sea un nullptr y del tipo correcto. Usando QNetworkReply::readAll() , la respuesta completa está disponible. Si slotFinished () se llama directamente (no a través de una señal/ranura), el reply el objeto será un nullptr. Hay algunas consideraciones más a tener en cuenta con QObject::sender() como si el objeto de origen se destruye y DirectConnection , pero para nuestro ejemplo esto funcionará bien.

La documentación menciona explícitamente llamar a deleteLater() en la respuesta de la red, por lo que hacemos eso en lugar de la eliminación normal.

La última parte de nuestro método es un nuevo Q_PROPERTY llamado response . Agréguelo en el encabezado justo debajo de la línea Q_OBJECT :

Q_PROPERTY(QString response READ response WRITE setResponse NOTIFY responseChanged)

En versiones recientes de Qt Creator, puede hacer clic con el botón derecho en Q_PROPERTY parte y seleccione Refactor , Generate Missing Q_PROPERTY Members . Haz eso, nada especial sobre esta propiedad de lo contrario. Si su versión de Qt Creator no muestra esa práctica opción, agregue la señal/ranura y la variable miembro manualmente.

En Qml, vincule esta propiedad al TextField text propiedad:

TextField {
    id: cppResult
    text: NetworkExample.response
}

Haz el Button llama a la función que acabamos de definir:

Button {
    text: "C++ HTTP GET "
    onClicked: NetworkExample.doGetRequest("http://httpbin.org/ip")
}

Esta URL devolverá una respuesta JSON que contiene la IP de envío.

Presiona el botón verde grande Play (ejecutar) y pruébalo:

Eso fue fácil, ¿verdad? No te metas con un CURL* o curl_easy_setopt() y asíncrono por defecto. La parte de QML/JavaScript es aún más fácil, tan fácil que se siente como hacer trampa sin seguridad.

Solicitud QML HTTP GET

La parte QML es simplemente JavaScript antiguo con un enlace de propiedad. En el main.qml archivo, defina un property var que contendrá los datos de respuesta, dentro del Window{} , justo encima de nuestro Column :

property var response: undefined

Justo debajo de la nueva propiedad, agregue una función que hará la solicitud:

function doGetRequest(url) {
    var xmlhttp = new XMLHttpRequest()
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState === XMLHttpRequest.DONE
                && xmlhttp.status == 200) {
            response = xmlhttp.responseText
        }
    }
    xmlhttp.open("GET", url, true)
    xmlhttp.send()
}

El método, cuando se llama, hace un XMLHttpRequest , con una función de devolución de llamada que verifica el código de estado, si la solicitud fue exitosa, actualiza el response propiedad. Vincule la propiedad de respuesta a nuestro TextField :

TextField {
    id: qmlResult
    text: response
}

Agregue la nueva función al botón onClicked :

Button {
    text: "Qml HTTP GET"
    onClicked: {
        response = ""
        doGetRequest("http://httpbin.org/ip")
    }
}

Adelante, presiona el gran botón verde Reproducir y pruébalo:

Por supuesto, podría, en el caso de JSON, agregar un JSON.parse(xmlhttp.responseText) , luego puede acceder al JSON directamente dentro de QML, (text: response.origin ), o agregar más manejo de errores.

Como puede ver, debido a que es solo JavaScript, esto es incluso más fácil que la ya muy simple parte de C++.

Si quieres probar el async -ness, específicamente, sin bloquear el subproceso GUI, use la url https://httpbin.org/delay/4 , que esperará 4 segundos antes de responder. Todavía debería poder hacer clic en los botones y ver cómo suceden las cosas.

Envíeme sus opiniones sobre lo que más le gusta, C ++ o Qml para este propósito.