Back-Link: https://www.hasenzaehn.ch/StaticHtml/wiki.html
Die Implementierung des QtHttpClient enthält einen QNetworkAccessManager.
Bei jedem GET-Call wird ein lokaler QEventLoop gestartet, sodass die Methode blockierend zurückkehrt – entweder mit Werten oder einer Error-Message, die in einem HttpError gesammelt wird.
namespace MaterialDataFetcher {
class QtHttpClient : public QObject, public IHttpClient {
Q_OBJECT
public:
explicit QtHttpClient(const std::string& baseUrl,
const std::string& apiKey = {},
QObject* parent = nullptr);
std::expected<nlohmann::json, HttpError>
get(const std::string& path) const override;
private:
std::expected<nlohmann::json, HttpError>
execRequest(QNetworkReply* reply) const;
QString m_baseUrl;
QString m_apiKey;
mutable QNetworkAccessManager m_qnam;
};
} // namespace MaterialDataFetcher
QNetworkAccessManager erbt von QObject → es ist threadgebunden an den Thread, in dem es erstellt wurde.
Das bedeutet: Alle Aufrufe wie m_qnam.get(...) müssen aus demselben Thread kommen.
Eine einzige Instanz von QNetworkAccessManager reicht für die gesamte Qt-Anwendung.
App erstellt einen QNetworkAccessManager. Methoden wie get(), post() liefern jeweils ein QNetworkReply zurück (ich brauche ja nut get)
QNetworkReply enthält die heruntergeladenen Daten sowie Metadaten (z. B. Header)
// aus .h
mutable QNetworkAccessManager m_qnam;
//...................................
std::expected<nlohmann::json, HttpError>
QtHttpClient::get(const std::string& path) const {
// QNetworkRequest Objekt erstellen
QNetworkReply* reply = m_qnam.get(request);
return execRequest(reply);
}
In execRequest() wird die QNetworkReply weiterverarbeitet: Da wird das JSON-Parsing mit nlohmann::json gemacht
Fehlerbehandlung und Rückgabe als std::expected - das muss ich dann noch korrekt auf die generischen Fehler-Codes abbilden.
Frage: Welche Daten will man überhaupt ziehen?
Antwort: Das Request-Objekt braucht zumindest:
Vorab kann man die API einfach über das Terminal testen:
curl -sSLG 'https://api.materialsproject.org/materials/summary/' \
--data-urlencode 'material_ids=mp-685097' \
--data-urlencode '_fields=material_id,formula_pretty,symmetry,structure,density' \
-H "X-API-KEY: $MP_API_KEY" \
| jq '.data[0] | {material_id, formula_pretty, space_group: .symmetry.symbol,
lattice: .structure.lattice | {a,b,c,alpha,beta,gamma}, density}'
{
"material_id": "mp-685097",
"formula_pretty": "HfO2",
"space_group": "Pca2_1",
"lattice": {
"a": 5.00476369,
"b": 5.03268505,
"c": 5.21946336,
"alpha": 90.0,
"beta": 90.0,
"gamma": 90.0
},
"density": 10.634789518075097
}
Das Request Objekt im QtHttpClient.cpp} erhält man genau gleich:
QUrl url(m_baseUrl + QString::fromStdString(path));
/* echtes Beispiel:
QUrl url("https://api.materialsproject.org/materials/summary/");
*/
QUrlQuery q;
q.addQueryItem("material_ids", "mp-685097");
q.addQueryItem("_fields", "material_id,formula_pretty,symmetry,structure,density");
url.setQuery(q);
QNetworkRequest request(url);
request.setRawHeader("Accept", "application/json");
if (!m_apiKey.isEmpty())
request.setRawHeader("X-API-KEY", m_apiKey.toUtf8());
Die QNetworkReply* reply muss nun verarbeitet werden und in ein nlohmann::json Objekt umgewandelt werden. Zudem braucht es die Fehlerbehandlung.
Ein Signal wird emittiert, wenn sich ein bestimmter Event ereignet.
Ein Slot ist eine Funktion, die als Reaktion auf das Signal aufgerufen wird.
Die Struktur sieht wie folgt aus:
connect(Object1, signal, Object2, signal);
https://doc.qt.io/qt-6/signalsandslots.html
- All classes that inherit from QObject or one of its subclasses can contain signals and slots.
- Signals and slots are loosely coupled: A class which emits a signal neither knows nor cares which slots receive the signal. Just as an object does not know if anything receives its signals, a slot does not know if it has any signals connected to it.
- if you connect a signal to a slot, the slot will be called with the signals parameters at the right time
- Signals are emitted by an object when its internal state has changed in some way that might be interesting to the object's client or owner.
- Execution of the code following the emit statement will occur once all slots have returned.
QNetworkAccessManager arbeitet komplett asynchron. Wenn man get() aufruft, bekommt man zwar sofort ein QNetworkReply*‑Objekt zurück, das aber noch nichts enthält. Die eigentlichen Netzwerk‑IO‑Operationen (DNS‑Auflösung, TCP‑Handshake, TLS‑Aushandlung, Übertragung der HTTP‑Nachricht usw.) laufen im Hintergrund‑Thread des Qt‑Event‑Dispatchers:
QAbstractEventDispatcherprovides fine-grained control over event delivery. The main event loop is started by callingQCoreApplication::exec(), and stopped by callingQCoreApplication::exit(). Local event loops can be created usingQEventLoop. https://doc.qt.io/qt-6/qabstracteventdispatcher.html
Damit der Code erst dann weiterläuft, wenn die Reply vollständig empfangen wurde, muss man zuerst auf das Signal QNetworkReply::finished() warten. Mit loop.exec() wird also ein EvenLoop gestartet, welcher nur läuft bis quit aufgerufen wird. Durch das Blockieren des aktuellen Threads bis zum Abschluss der Netzwerk‑Operation stellt man sicher, dass die Antwort also vollständig vorhanden ist.
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
Das Objekt QNetworkReply emittiert ein Signal QNetworkReply::finished(), sobald die reply fertig prozessiert ist. Das heisst es gibt keine weiteren Updates zu den Reply-Daten oder seinen Metadaten. Aber solange das Signal close()/abort() nicht ausgeführt ist, bleibt reply offen für das Lesen. Die Datenpunkte können also mit read() oder readAll() ausgelesen werden - es wird ein QByteArray zurück gegeben.
Sobald also das Prozessieren des Reply-Objekts fertig ist, wird dem EventLoop gesagt, dass er sich beenden kann.
Damit loop.exec() im dümmsten Fall nicht ewigs hängen bleibt, weil es zb einen Server-Ausfall oder es eine Firewall-Blockade gibt, muss man ein Timeout definieren. Der Timer läuft parallel zum Netzwerk-I/O. Ist der Timer abgezählt, wird reply->abort() aufgerufen, dies führt zu einem error(QNetworkReply::OperationCanceledError) und gleichzeitig kommt es zur Beendigung des EvenLoops, weil abort() intern
QNetworkReply::finished() aufruft.
QTimer timer;
timer.setSingleShot(true);
QObject::connect(&timer, &QTimer::timeout, &loop, [&]{
reply->abort();
});
timer.start(30000);
loop.exec(); //das blockiert also bis die Antwort da ist, oder der Timer fertig
Nun kann man prüfen, was genau geschehen ist, kam die Antwort an? Ist ein Fehler aufgetreten?
Unter https://doc.qt.io/qt-6/qnetworkreply.html findet man die unterschiedlichen QNetworkReply::NetworkError, die man unterscheiden kann (und später auf den FetchError abbilden muss).
static std::optional<HttpError> mapQtError(QNetworkReply* reply) {
using NE = QNetworkReply::NetworkError;
if (reply->error() != QNetworkReply::NoError) {
switch (reply->error()) {
case NE::TimeoutError:
case NE::OperationCanceledError:
return HttpError{ HttpError::Code::Timeout, reply->errorString().toStdString() };
case NE::SslHandshakeFailedError:
return HttpError{ HttpError::Code::Ssl, reply->errorString().toStdString() };
default:
return HttpError{ HttpError::Code::Network, reply->errorString().toStdString() };
}
}
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status >= 300) {
if (status == 401 || status == 403)
return HttpError{ HttpError::Code::Auth, "HTTP " + std::to_string(status) };
if (status == 429)
return HttpError{ HttpError::Code::RateLimited, "HTTP 429" };
return HttpError{ HttpError::Code::HttpStatus, "HTTP " + std::to_string(status) };
}
return std::nullopt;
}