Работа с сетью в Qt

QNetworkAccessManager: GET/POST-запросы и обработка JSON-ответа

Введение

Большинство современных Qt-приложений взаимодействуют с веб-сервисами через REST API, обмениваясь данными в формате JSON. Основной инструмент для этого в Qt — QNetworkAccessManager, который умеет выполнять HTTP-запросы асинхронно, поддерживает редиректы, куки, прокси и SSL. В статье рассмотрим типичные GET и POST запросы, отправку JSON в теле запроса и разбор JSON-ответа.

Концепция

QNetworkAccessManager принимает объект QNetworkRequest (URL, заголовки) и возвращает QNetworkReply — объект, который сигнализирует о завершении запроса через finished(). Важно: QNetworkReply — это QIODevice, и его нужно явно удалять после обработки (обычно через deleteLater() в самом обработчике finished), иначе реплаи будут накапливаться в памяти.

Для отправки JSON в POST-запросе тело формируется как QByteArray через QJsonDocument::toJson(), а заголовок Content-Type: application/json указывается явно — Qt не делает этого автоматически. Один и тот же QNetworkAccessManager следует создавать один раз на приложение (или на модуль) и переиспользовать для всех запросов — он держит пул соединений и кеш, и создание нового экземпляра на каждый запрос неэффективно.

Пример кода: QNetworkAccessManager

#include <QCoreApplication>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>

class ApiClient : public QObject
{
    Q_OBJECT
public:
    explicit ApiClient(QObject *parent = nullptr) : QObject(parent) {}

    void fetchUser(int userId)
    {
        QUrl url(QString("https://jsonplaceholder.typicode.com/users/%1").arg(userId));
        QNetworkRequest request(url);
        QNetworkReply *reply = m_manager.get(request);
        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
            handleReply(reply);
        });
    }

    void createPost(const QString &title, const QString &body)
    {
        QUrl url("https://jsonplaceholder.typicode.com/posts");
        QNetworkRequest request(url);
        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

        QJsonObject payload;
        payload["title"] = title;
        payload["body"] = body;
        payload["userId"] = 1;

        QNetworkReply *reply = m_manager.post(request, QJsonDocument(payload).toJson());
        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
            handleReply(reply);
        });
    }

private:
    void handleReply(QNetworkReply *reply)
    {
        reply->deleteLater();

        if (reply->error() != QNetworkReply::NoError) {
            qWarning() << "Ошибка запроса:" << reply->errorString();
            return;
        }

        const QByteArray data = reply->readAll();
        QJsonParseError parseError;
        const QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
        if (parseError.error != QJsonParseError::NoError) {
            qWarning() << "Ошибка разбора JSON:" << parseError.errorString();
            return;
        }

        qDebug() << "Ответ сервера:" << doc.toJson(QJsonDocument::Compact);
    }

    QNetworkAccessManager m_manager;
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    ApiClient client;
    client.fetchUser(1);
    client.createPost("Заголовок", "Текст сообщения");
    return app.exec();
}

#include "main.moc"

Пояснения к коду

Метод fetchUser() выполняет простой GET-запрос, а createPost() формирует JSON-объект через QJsonObject, сериализует его в QByteArray и отправляет POST-запросом с явно указанным заголовком Content-Type. Общий обработчик handleReply() проверяет код ошибки через reply->error(), читает тело ответа и парсит его в QJsonDocument, отдельно обрабатывая ошибку разбора JSON.

Вызов reply->deleteLater() в начале обработчика гарантирует, что объект будет корректно удалён даже если далее произойдёт ранний return при ошибке.

Подводные камни

  • Не проверять reply->error() перед чтением данных. При ошибке сервер может вернуть тело с описанием ошибки (например, HTML-страницу 404), и попытка распарсить его как JSON приведёт к путанице в диагностике.
  • Забыть deleteLater() у QNetworkReply. Каждый невыполненный запрос держит выделенную память; в долгоживущем приложении с частыми запросами это приводит к утечке.
  • Создание QNetworkAccessManager на каждый запрос. Это разрывает переиспользование соединений (HTTP keep-alive) и кеша, что заметно снижает производительность при множестве запросов.
  • Лямбды, захватывающие this в объекте, который может быть удалён до завершения запроса. Если объект-владелец удаляется раньше ответа, обработчик обращается к освобождённой памяти — используйте QPointer или connect с контекстным объектом для автоматического отключения сигнала.