Framework Qt często kojarzony jest głównie z tworzeniem graficznego interfejsu użytkownika. Jednak to potężne narzędzie ma znacznie więcej do zaoferowania, co często umyka uwadze wielu inżynierów. Qt wyposażony jest w szereg modułów rozszerzających jego funkcjonalność poza GUI, takich jak:
- obsługa sieci,
- grafika 3D,
- manipulacja danymi w formacie JSON, lub XML.
W dzisiejszym artykule skoncentrujemy się na mniej oczywistym aspekcie Qt – jego zdolnościach do tworzenia aplikacji sieciowych. Przejdziemy przez proces tworzenia małego klienta REST API oraz mini serwera HTTP, pokazując, jak te zaawansowane funkcje mogą być wykorzystane w praktycznych projektach programistycznych.
REST API
Na początku przypomnijmy sobie, czym jest REST (czyli Representation State Transfer).
REST to styl architektury oprogramowania zapewniający skalowalność, prostotę, niezależność komponentów, a także efektywność w przesyłaniu danych między klientem a serwerem.
Cechuje się:
- bezstanowością (statelessness),
- oddzieleniem klienta od serwera (client-server separation),
- jednolitym interfejsem (uniform interface),
- bezpiecznym połączeniem przez warstwę,
- obsługą pamięci podręcznej,
- systemem warstwowym.
Z pojęciem REST API najczęściej łączony jest protokół HTTP (Hypertext Transfer Protocol) wykorzystywany w usługach internetowych. Bazując na bezstanowości architektury, każde żądanie HTTP do serwera musi zawierać wszystkie niezbędne informacje do jego zrozumienia i wykonania, bez potrzeby zachowywania wcześniejszego kontekstu sesji. REST API korzystające z protokołu HTTP bazuje na metodach HTTP.
Wśród nich najczęściej używanymi są:
- GET – wykorzystywana do pobierania danych o zasobach,
- POST – tworzy nowy zasób, wykorzystywana również do innych operacji, gdy nie mieszczą się one w ramach pozostałych metod,
- DELETE – usuwa określony zasób,
- PUT – modyfikuje/aktualizuje dany zasób na podstawie identyfikatora.
Qt a usługi sieciowe
W Qt dostęp do usług sieciowych jest realizowany przez bibliotekę Network. Dzięki klasom zawartym w tym module, programowanie sieciowe oparte na takich protokołach jak HTTP, TCP czy UDP staje się prostsze i bardziej intuicyjne.
Dodanie modułu do projektu jest bardzo łatwe. Jeśli projekt jest budowany w oparciu o qmake, wystarczy dodać następującą linie:
QT += network
Natomiast w przypadku CMake realizuje się to w następujący sposób:
find_package(Qt6 REQUIRED COMPONENTS Network)
target_link_libraries(mytarget PRIVATE Qt6::Network)
Jednym z atutów biblioteki Qt jest bogata dokumentacja, o całej zawartości modułu sieciowego można przeczytać w Network Programming API.
Klient REST API
W pierwszej kolejności zaimplementujemy klienta REST API wysyłającego żądania: GET, POST i DELETE oraz wyświetlającego odpowiedzi od serwera. Do komunikacji sieciowej wykorzystamy klasę QNetworkAccessManager umożliwiającą wysyłanie żądań HTTP oraz odbieranie odpowiedzi z serwera w sposób asynchroniczny, czyli nieblokujący interfejsu użytkownika oraz pętli głównej aplikacji. Ponadto przechowuje ona wspólną konfigurację i ustawienia dla wysyłanych żądań. Zaleca się, aby na całą aplikację stworzona była tylko jedna instancja QNetworkAccessManager.
Przykładowa implementacja klienta z wykorzystaniem klasy QNetworkAccessManager:
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QJsonDocument>
const QString JSONContentTypeHeader = "application/json";
class RESTClient : public QObject {
Q_OBJECT
public:
RESTClient(QObject * parent = nullptr) : QObject(parent) {
m_manager = new QNetworkAccessManager(this);
connect(m_manager, &QNetworkAccessManager::finished, this, &RESTClient::onFinished);
}
void sendGetRequest(const QUrl &url) {
QNetworkRequest request(url);
m_manager->get(request);
}
void sendPostRequest(const QUrl &url, const QByteArray &data) {
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, JSONContentTypeHeader);
m_manager->post(request, data);
}
void sendDeleteRequest(const QUrl &url) {
QNetworkRequest request(url);
m_manager->deleteResource(request);
}
private slots:
void onFinished(QNetworkReply *reply) {
if (reply->error() == QNetworkReply::NoError) {
qDebug() << "Response received:" << reply->readAll();
} else {
qDebug() << "Error:" << reply->errorString();
}
reply->deleteLater();
}
private:
QNetworkAccessManager *m_manager;
};
W powyższym przykładzie klasa RESTClient odpowiedzialna jest za wysyłanie żądań przez zaimplementowane metody:
- sendGetRequest – wysyła żądanie typu GET,
- sendPostRequest – wysyła żądanie typu POST,
- sendDeleteRequest – wysyła żądanie typu DELETE.
Jak można łatwo zauważyć, stworzenie i wysłanie żądania do serwera jest bardzo proste. Do każdego z typów zapytania klasa QNetworkAccessManager posiada odpowiednie metody. Programista odpowiedzialny jest wyłącznie za przygotowanie poprawnego żądania oraz obsługę wyniku otrzymanego z serwera.
Gdy klasa QNetworkAccessManager otrzymuje odpowiedź z serwera, emituje sygnał finished(QNetworkReply *reply), przekazując wskaźnik do tej odpowiedzi jako argument. W slocie onFinished(QNetworkReply *reply) sprawdzane są szczegóły odpowiedzi, w tym zawartość oraz ewentualne błędy serwera, co umożliwia odpowiednie reagowanie na otrzymane dane. W tym przypadku programista jest zobowiązany do zarządzania pamięcią i usunięcia wskaźnika reply.
Do tego wykorzystana została metoda QObject::deleteLater(), która sprawia, że obiekt jest niszczony przy następnej iteracji pętli głównej. Jest to zalecany sposób usuwania obiektów pochodzących od klasy QObject. Stosując deleteLater zamiast delete, unikamy sytuacji, w której usunięto obiekt, natomiast zdarzenia z nim związane pozostały do obsłużenia w aktualnej iteracji pętli.
W obecnej implementacji klasy RESTClient cała odpowiedzialność za odbiór i przetwarzanie danych z serwera spoczywa na niej samej, co może nie być optymalne. Z tego powodu planujemy zmodyfikować implementację tak, aby to klasa wykorzystująca RESTClient zarządzała przetwarzaniem otrzymanych informacji. To pozwoli na większą elastyczność i lepsze zarządzanie odpowiedziami z serwera.
#include <QtNetwork/QNetworkAccessManager>
const QString JSONContentTypeHeader = "application/json";
class RESTClient : public QObject {
Q_OBJECT
public:
RESTClient(QObject * parent = nullptr) : QObject(parent) {
m_manager = new QNetworkAccessManager(this);
}
QNetworkReply* sendGetRequest(const QUrl &url) {
QNetworkRequest request(url);
return m_manager->get(request);
}
QNetworkReply* sendPostRequest(const QUrl &url, const QByteArray &data) {
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, JSONContentTypeHeader);
return m_manager->post(request, data);
}
QNetworkReply* sendDeleteRequest(const QUrl &url) {
QNetworkRequest request(url);
return m_manager->deleteResource(request);
}
private:
QNetworkAccessManager *m_manager;
};
Po wprowadzonych zmianach klasa RESTClient pełni jedynie funkcję wysyłania żądań, a obiekt zawierający odpowiedź jest bezpośrednio zwracany do warstwy wyższej, która korzysta z klienta.
Przykład użycia:
#include <QCoreApplication>
#include "RESTClient.h"
#include <QNetworkReply>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
RESTClient client;
auto reply = client.sendGetRequest(QUrl("http://httpbin.org/get"));
QObject::connect(reply, &QNetworkReply::finished, [reply](){
qDebug() << reply->readAll();
reply->deleteLater();
qApp->exit();
});
return a.exec();
}
#include "main.moc"
Główną zaletą tej implementacji jest użycie sygnału QNetworkReply::finished() zamiast QNetworkAccessManager::finished(QNetworkReply *). Bezpośrednie działanie na obiektach klasy QNetworkReply pozwala na rozdzielenie logiki aplikacji. Jednocześnie sprawiając, że możliwa jest obsługa odpowiedzi przez bardziej rozbudowanie obiekty, co powoduje, że kod staje się bardziej elastyczny i czytelny.
QNetworkRequest
W powyższych przykładach podczas wysyłania zapytań do serwera wykorzystywaliśmy klasę QNetworkRequest. Reprezentuje ona żądanie sieciowe w komunikacji po HTTP w Qt.
Do tej pory używaliśmy jej w dość prosty sposób poprzez utworzenie obiektu i przekazywanie URL do konstruktora, tak jak poniżej:
QUrl url("http://httpbin.org/get");
QNetworkRequest request(url);
Klasa QNetworkRequest umożliwia ustawianie nagłówków HTTP, które mogą zawierać dodatkowe informacje takie jak typ danych, dane autoryzacji, wersję API oraz wiele innych. W aplikacjach wykorzystujących REST API dość często dodatkowe nagłówki są wymagane do prawidłowego przetworzenia zapytania przez system.
Nagłówki dodaje się do żądania poprzez użycie metody setHeader, która jako pierwszy argument przyjmuje typ wyliczeniowy QNetworkRequest::KnownHeaders, natomiast drugim argumentem jest przesyłana wartość. QNetworkRequest::KnownHeaders zawiera najbardziej popularne i najczęściej wykorzystywane nagłówki HTTP. Do dodawania nietypowych nagłówków, które nie są zawarte w typie wyliczeniowym, używamy metody setRawHeader, gdzie pierwszym argumentem jest nazwa przesyłanego nagłówka.
Przykład ustawiania nagłówków:
QNetworkRequest request(QUrl("http://httpbin.org/get"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "BearerAccessToken");
Ponadto możliwe jest ustawianie dodatkowych atrybutów w zapytaniach. Atrybuty odpowiedzialne są za wiele przydatnych funkcji takich jak:
- ustawienie priorytetu żądania,
- kontrola nad polityką przekierowań,
- obsługa cache,
- korzystanie z atrybutów specyficznych dla protokołu.
Wszystkie reprezentowane są przez typ wyliczeniowy QNetworkRequest::Attribute, a ich wartości przechowywane są w klasie QVariant, szczegóły opisane są w dokumentacji.
Ustawianie atrybutów żądania jest tak samo proste jak dodawanie nagłówków. Używamy do tego metody setAttribute, przykład:
QNetworkRequest request(QUrl("http://httpbin.org/get"));
request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
Serwer HTTP
Do napisania własnego serwera obsługującego żądania HTTP skorzystamy z klasy QHttpServer należącej do modułu HttpServer.
QHttpServer posiada w swojej funkcjonalności:
- Routing żądań – pozwala zdefiniować, jak serwer powinien reagować na różnego rodzaju żądania, takie jak GET, POST, DELETE itp. Wykorzystuje się do tego metodę
route(), która przyjmuje ścieżkę, metodę HTTP i funkcję obsługi, co umożliwia elastyczne zarządzanie logiką biznesową. - Obsługę odpowiedzi – dla każdego typu żądania można zdefiniować odpowiedź, która będzie zwracana. Może to być tekst, JSON, lub inny format danych. Serwer może również obsługiwać błędy i inne typowe scenariusze HTTP.
- Integrację z
Qt Core– dzięki integracji z pętlą zdarzeń Qt,QHttpServerdziała asynchronicznie i efektywnie, zarządzając wieloma połączeniami bez blokowania głównego wątku aplikacji.
Ten mechanizm sprawia, że QHttpServer jest idealny do tworzenia lekkich serwerów HTTP w aplikacjach, które potrzebują lokalnego backendu.
Przykład użycia:
#include <QCoreApplication>
#include <QHttpServer>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QStringList users = {"testUserName"};
QHttpServer server;
server.route("/users", QHttpServerRequest::Method::Get, [&users] () {
return QHttpServerResponse(QJsonArray::fromStringList(users), QHttpServerResponse::StatusCode::Ok);
});
server.route("/users", QHttpServerRequest::Method::Post, [&users](const QHttpServerRequest &request) {
QJsonDocument doc = QJsonDocument::fromJson(request.body());
const QJsonObject obj = doc.object();
const QString name = obj["name"].toString();
if (name.isEmpty()) {
return QHttpServerResponse("Invalid data", QHttpServerResponse::StatusCode::BadRequest);
}
if (users.contains(name)) {
return QHttpServerResponse("User exists", QHttpServerResponse::StatusCode::BadRequest);
}
users.append(name);
return QHttpServerResponse("User added", QHttpServerResponse::StatusCode::Ok);
});
server.route("/users/<arg>", QHttpServerRequest::Method::Delete, [&users] (const QString& name) {
if (name.isEmpty() || !users.contains(name)) {
return QHttpServerResponse("Invalid data", QHttpServerResponse::StatusCode::BadRequest);
}
users.removeAll(name);
return QHttpServerResponse(QHttpServerResponse::StatusCode::Ok);
});
const int port = 8080;
if (server.listen(QHostAddress::Any, port)) {
qDebug() << "Start listening on port:" << port;
} else {
qDebug() << "Cannot start listening on port:" << port;
exit(1);
}
return a.exec();
}
W powyższym przykładzie zaimplementowane są trzy metody HTTP:
- GET /users – zwracająca użytkowników w postaci listy JSON,
- POST /users – dodawanie użytkownika do listy; nazwa użytkownika jest odczytywana z ciała żądania poprzez metodę
request.body(), - DELETE /users/<arg> – usuwanie użytkownika z listy, gdzie
<arg>to nazwa użytkownika do usunięcia. W przypadku, gdy ma być usunięty użytkownik o nazwietestUser, żądanie powinno wyglądać następująco:DELETE /users/testUser.
Serwer uruchamiany jest poprzez wywołanie metody QHttpServer::listen().

Wnioski
W dzisiejszym artykule przedstawiłem Wam, jak w łatwy sposób napisać dwie aplikacje komunikujące się po HTTP, zarówno część kliencką jak i serwerową.
Qt oferuje bogaty zestaw narzędzi, które pozwalają nie tylko na tworzenie atrakcyjnych interfejsów graficznych, ale również na efektywne i elastyczne tworzenie aplikacji sieciowych. Dzięki bogatej zawartości frameworka, tworzenie zaawansowanych aplikacji jest łatwe i przyjemne, zachowując dużą wydajność. Bez nakładu znacznych ilości czasu i pracy możliwe jest utworzenie działającej aplikacji, dzięki czemu staje się ono narzędziem do szybkiego prototypowania oraz tworzenia gotowych rozwiązań.
Zachęcam do eksploracji możliwości, jakie otwiera Qt, zwłaszcza w kontekście rozwoju oprogramowania sieciowego, gdzie jego zaawansowane moduły sieciowe mogą znacząco przyspieszyć i ułatwić rozwój złożonych aplikacji. Niech inspiracje i przykłady przedstawione w tym wpisie będą punktem wyjścia do własnych eksperymentów i projektów z wykorzystaniem Qt 😊
***
Jeżeli interesuje Cię obszar frameworka Qt, zajrzyj koniecznie również do innych artykułów naszych specjalistów.
Zostaw komentarz