Писал я как-то мессенджер для «ВКонтакте» и пришлось мне разбираться с отправкой GET/POST запросов. Оказалось, для этих целей в Qt можно (и нужно) использовать класс из модуля QtNetwork — QNetworkAccessManager, а также QNetworkReply и QNetworkRequest. Для GET/POST запросов у этого класса есть соответствующие методы get, который принимает QNetworkRequest и post, который помимо QNetworkRequest принимает еще и QByteArray (собственно данные, которые нужно отправить). В итоге я написал свою обертку над QNetworkAccessManager, которая полностью скрывает работу с этими классами. Кому интересно — прошу под кат.

Мне нужен был простой интерфейс для отправки GET/POST запросов на указанный url, с возможностью добавления параметров, установки прокси и таймаута. Для этих целей были созданы всего два класса Request и RequestSender.

#ifndef NETWORK_REQUEST_H
#define NETWORK_REQUEST_H

namespace Network
{
	class Request
	{
	public:
		Request(QString address = QString());

		QString address() const;
		void setAddress(QString address);

		void addParam(QString name, QVariant value);
		bool removeParam(QString name);

		QStringList paramsNames() const;
		QMap<QString, QString> params() const;

		QUrl url(bool withParams = true) const;
		QNetworkRequest request(bool withParams = true) const;
		QByteArray data() const;

	private:
		QString _address;
		QMap<QString, QString> _params;
	};
}

#endif // NETWORK_REQUEST_H


#include "stdafx.h"
#include "request.h"

namespace Network
{

	Request::Request(QString address /*= QString()*/)
	{
		setAddress(address);
	}

	QString Request::address() const
	{
		return _address;
	}

	void Request::setAddress(QString address)
	{
		for (QPair<QString, QString> value : QUrlQuery(QUrl(address)).queryItems())
			addParam(value.first, value.second);
		_address = address;
	}

	void Request::addParam(QString name, QVariant value)
	{
		_params[name] = value.toString();
	}

	bool Request::removeParam(QString name)
	{
		if (false == _params.contains(name))
			return false;
		_params.remove(name);
		return true;
	}

	QStringList Request::paramsNames() const
	{
		return _params.keys();
	}

	QMap<QString, QString> Request::params() const
	{
		return _params;
	}

	QUrl Request::url(bool forGetRequest /*= true*/) const
	{
		QUrl url(address());
		if (forGetRequest)
			url.setQuery(data());
		return url;
	}

	QNetworkRequest Request::request(bool forGetRequest /*= true*/) const
	{
		QNetworkRequest r(url(forGetRequest));

		if (!forGetRequest)
		{
			r.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
		}

		return r;
	}

	QByteArray Request::data() const
	{
		auto b = _params.begin();
		auto e = _params.end();

		QByteArray byteArrayData;

		while (b != e)
		{
			byteArrayData.append(b.key());
			byteArrayData.append('=');
			byteArrayData.append(b.value());
			byteArrayData.append('&');

			b++;
		}

		byteArrayData.chop(1);

		return byteArrayData;
	}

}

В объяснении, я думаю, не нуждается поэтому идём дальше — к классу RequestSender.

#ifndef NETWORK_REQUESTSENDER_H
#define NETWORK_REQUESTSENDER_H

#include "request.h"

namespace Network
{
	class RequestSender : public QObject
	{
		Q_OBJECT
	public:

		enum RequestError
		{
			NoError,
			TimeoutError
		};

		RequestSender(qint64 maxWaitTime = 35000);
		~RequestSender();

		void setProxy(const QNetworkProxy& proxy);

		QByteArray get(Request& request);
		QByteArray post(Request& request);
		QByteArray getWhileSuccess(Request& request, int maxCount = 2);
		QByteArray postWhileSuccess(Request& request, int maxCount = 2);
		
		void setMaxWaitTime(qint64 max);

		qint64 maxWaitTime() const;
		RequestError error() const;

	private:
		QByteArray sendRequest(Request& request, bool getRequest = true);
		QByteArray sendWhileSuccess(Request& request, int maxCount = 2, bool getRequest = true);

	private:
		qint64 _maxWaitTime;
		RequestError _error;
		QNetworkProxy _proxy;
	};
}


#endif // NETWORK_REQUESTSENDER_H


#include "stdafx.h"
#include "requestsender.h"

namespace Network
{
	RequestSender::RequestSender(qint64 maxWaitTime /*= 35000*/)
	{
		setMaxWaitTime(maxWaitTime);
		_error = NoError;
	}

	RequestSender::~RequestSender()
	{

	}

	void RequestSender::setProxy(const QNetworkProxy& proxy)
	{
		_proxy = proxy;
	}

	QByteArray RequestSender::get(Request& request)
	{
		return sendRequest(request, true);
	}

	QByteArray RequestSender::post(Request& request)
	{
		return sendRequest(request, false);
	}

	QByteArray RequestSender::getWhileSuccess(Request& request, int maxCount /*= 2*/)
	{
		return sendWhileSuccess(request, maxCount, true);
	}

	QByteArray RequestSender::postWhileSuccess(Request& request, int maxCount /*= 2*/)
	{
		return sendWhileSuccess(request, maxCount, false);
	}

	void RequestSender::setMaxWaitTime(qint64 max)
	{
		_maxWaitTime = max;
	}

	qint64 RequestSender::maxWaitTime() const
	{
		return _maxWaitTime;
	}

	RequestSender::RequestError RequestSender::error() const
	{
		return _error;
	}

	QByteArray RequestSender::sendRequest(Request& request, bool getRequest /*= true*/)
	{
		QTimer timer;
		timer.setInterval(_maxWaitTime);
		timer.setSingleShot(true);

		QEventLoop loop;
		QSharedPointer<QNetworkAccessManager> manager(new QNetworkAccessManager);
		manager->setProxy(_proxy);

		QNetworkReply* reply = getRequest ? manager->get(request.request()) :
											manager->post(request.request(false), request.data());

#if defined(NETWORK_SHOW_SEND_REQUESTS)
		if (getRequest)
			qDebug() << "[GET] " <<  request.request().url().toString();
		else
			qDebug() << "[POST]" << request.request(false).url().toString() << request.data(); 
#endif

		QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
		QObject::connect(&timer, &QTimer::timeout, reply, &QNetworkReply::abort);

		timer.start();
		loop.exec();

		QByteArray data;

		if (reply->isFinished() && reply->error() == QNetworkReply::NoError)
		{
			data = reply->readAll();
			_error = RequestSender::NoError;
		}
		else
		{
			_error = RequestSender::TimeoutError;
		}

		reply->deleteLater();

#if defined(NETWORK_SHOW_SEND_REQUESTS)
		qDebug() << "[ANSWER]" << data;
#endif

		return data;
	}

	QByteArray RequestSender::sendWhileSuccess(Request& request, int maxCount /*= 2*/, bool getRequest /*= true*/)
	{
		if (maxCount < 0)
			throw QString(__LINE__ + " " __FILE__);

		int c = 0;
		QByteArray answer;

		while (c < maxCount)
		{
			c++;
			answer = getRequest ? get(request) : post(request);

			if (error() == NoError)
				break;

			qDebug() << "Ошибка при отправке запроса. Код ошибки - " << error() << ". Повторная отправка запроса через 2 секунды\n";
			QThread::currentThread()->msleep(2000);
		}

		return answer;
	}

}


Тут наибольший интерес должен представлять метод sendRequest, в котором отправляется запрос, который имеет таймаут. После вызова этого метода можно узнать, прошел ли запрос удачно. Для этого есть метод error(), который вернет значение типа RequestError — NoError или TimeoutError.

В методе sendRequest создается таймер, который сработает только один раз, и ему устанавливается интервал равный таймауту. После создается QEventLoop и QNetworkAccessManager, в зависимости от типа запроса (POST/GET) вызывается соответствующий метод, после связываем сигналы, запускаем таймер и переходим в цикл обработки событий созданного нами QEventLoop. Этот цикл прервется в одном случае — reply отправил сигнал finished. Этот сигнал он отправит в двух случаях — либо запрос выполнился, либо был отменен в связи с наступлением таймаута.

После идёт проверка — закончился ли запрос успешно, если да то считываем данные из ответа, устанавливаем _error в NoError, удаляем reply и возвращаем считанные данные. Иначе устанавливаем _error в TimeoutError. Для упрощения отладки был добавлен вывод отправляемых запросов и получаемых данных.

Иногда есть необходимость дать запросу несколько шансов, для этого есть методы getWhileSuccess и postWhileSuccess, которые сводятся к вызову sendWhileSuccess.

Я вынес всё это в отдельную библиотеку, которая уже не раз меня выручала, так как она сильно упрощает мне работу, когда нужна отправка запросов.

На этом всё. Надеюсь, кому-то это будет полезным.

Комментарии (25)


  1. Laert
    11.11.2015 14:41
    +1

    из документации советуют использовать один QNetworkAccessManager для всего приложения, как вариант вынести твой RequestSender в отдельный поток, а тайм ауты сделать частью


    1. Laert
      11.11.2015 14:48

      рубанули эликтричество =( у нас тут с этим плохо


    1. ArmanPrestige
      11.11.2015 19:18

      Не соглашусь с использованием одного QNetworkAccessManager-а на всё приложение. Бывают случаи, когда нужно отправлять запросы через прокси, причём прокси не один, а несколько. Тогда я создаю несколько RequestSender-ов и отправляю запросы с использованием прокси в разных потоках.


      1. Laert
        12.11.2015 11:52
        +1

        Ну раз зашла речь то неплохо бы еще тебе проверять редиректы вроде такого

        if (statusCode >= 300 && statusCode < 400) {
                return kRedirect;
        }


  1. Laert
    11.11.2015 14:47
    +1

    Рассмотри возможность вынести RequestSender в отдельный поток сделав менеджер его частью. Там же хранить сет реквестов и их id. Тайм ауты можно контролировать через QSignalMapper получаю в итоге id request который отвалился


    1. savostin
      11.11.2015 19:11

      Если не ошибаюсь, функции асинхронные, зачем в поток-то?


      1. ArmanPrestige
        11.11.2015 19:19

        Асинхронные. Одна из задач класса RequestSender — скрыть эту асинхронность.


        1. savostin
          11.11.2015 19:38

          Та я не Вам ;)


  1. Zifix
    11.11.2015 19:26
    +1

    Работа с REST API в Qt, больная тема, конечно, приходится писать приличных размеров велосипеды — и все осложняется недостатком информации как правильно реализовывать нюансы этой работы.

    Как уже писали выше, решение использовать QEventLoop выглядит странным, вследствии чего выполняется не менее странная проверка успешности выполнения в том же методе, без использования механизма сигналов/слотов. RequestError имеет всего два состояния, хватило бы возвращать bool. Хотя по сути ошибок может быть значительно больше, что в этой реализации не отлавливается в соответствующем слоте. Исключение как стандартный метод возврата результата в Qt — жуткий моветон, заставлять текущий (возможно главный) поток спать в бесконечном цикле, опять же без сигналов/слотов — быдлокод, и т.д.

    Не хотелось бы отбить желание выкладывать код, но если честно, то Request выглядит не сильно полезнее стандартной связки QUrl + QUrlQuery, Network же с одной стороны примитивный, с другой стороны написан криво :/


    1. ArmanPrestige
      11.11.2015 19:39

      RequestError имеет всего два состояния, хватило бы возвращать bool. Хотя по сути ошибок может быть значительно больше

      Да, именно потому, что ошибок может быть больше, не возвращается bool. Для моих целей мне, пока что, хватило этих двух состояний. Однако, было бы неплохо обрабатывать и другие случаи.
      заставлять текущий (возможно главный) поток спать в бесконечном цикле, опять же без сигналов/слотов — быдлокод, и т.д.

      А какую реализацию предложите для «избавления» от асинхронности QNetworkAccessManager? Не сказал бы «спал», скорее ожидание события. Если бы вы использовали другую библиотеку, которая работает не асинхронно, то у вас и там главный поток «спал», пока не придёт ответ. Я пока ничего лучшего кроме как связки с QEventLoop-ом не придумал.


      1. Zifix
        11.11.2015 19:43

        Я пока ничего лучшего кроме как связки с QEventLoop-ом не придумал.
        Стандартный случай для этого — очередь. В связке с со слотом, подписанным на finished(), конечно.


      1. Zifix
        11.11.2015 20:52

        Upd:

        Не сказал бы «спал», скорее ожидание события
        И в это время он ничего не делает, верно? Спит в ожидании события, хорошо.
        Если бы вы использовали другую библиотеку, которая работает не асинхронно, то у вас и там главный поток «спал», пока не придёт ответ.
        Давайте еще микроконтроллеры вспомним, ага. У нас ведь главный поток отвечает за отрисовку интерфейса пользователя, и если он спит пока идет запрос (максимум 2*n секунд) — UI не отвечает на внешние раздражители.


        1. ArmanPrestige
          11.11.2015 20:59

          Давайте еще микроконтроллеры вспомним, ага. У нас ведь главный поток отвечает за отрисовку интерфейса пользователя, и если он спит пока идет запрос (максимум 2*n секунд) — UI не отвечает на внешние раздражители.

          Никаких фризов не происходит, интерфейс отрисовывается и реагирует на все события. Благодаря использованию QEventLoop::exec.


          1. Zifix
            11.11.2015 21:04

            Если мы вызываем sendWhileSuccess и у нас все maxCount раз неудачно проходит запрос — то никаких фризов нет?


            1. ArmanPrestige
              11.11.2015 21:31

              Нет.


              1. Zifix
                12.11.2015 06:28

                Что по-вашему фризы? Вот мы без перерыва кликаем, например, в чекбокс во время этого действа, и он мгновенно меняет свое состояние сколько бы мы не кликали?


          1. midday
            11.11.2015 21:45
            +1

            loop.exec(); Фризит в том то и дело. Он не отпустит пока не придет сигнал finished, это и есть способ сделать из асинхронной функции синхронную. А тем более вы вообще sleep потом вызываете.
            Вы же сами писали «Одна из задач класса RequestSender — скрыть эту асинхронность.».
            Чтобы гуи рисовались надо в циклах вызывать QApplication::processEvents(), можно там к примеру таймер завести 30 раз в секунду вызывать QApplication::processEvents().

            А если честно, то я вообще не пойму поставленной задачи. QNetworkAccessManager простой как кирпич. Зачем все это объясните пожалуйста? Создал приемник сигналов от reply и радуешься. Хоть прогрессбары делай, хоть что… Отправил запрос и забыл. Ждешь в слоте.


            1. ArmanPrestige
              11.11.2015 21:50
              -1

              Какой sleep? Никакой sleep не вызывается. Я ещё раз говорю, что не фризится гуи. Соберите и проверьте сами.

              QNetworkAccessManager простой как кирпич.

              Но не всегда удобный. Чтобы отправить один запрос, создавать QNetworkAccessManager, соединять сигналы со слотами (ещё и слот создавать), потом заботится об удалении QNetworkReply… Слишком много «движений» для отправки одного запроса. А мой класс всё это скрывает.


              1. Zifix
                12.11.2015 06:05

                Какой sleep? Никакой sleep не вызывается.
                QThread::currentThread()->msleep(2000);
                


            1. Zifix
              12.11.2015 07:15

              loop.exec(); Фризит в том то и дело...
              Тут вы не правы, что не удивительно — с таким извращением встречаешься не каждый день. Очередь событий общая для потока, и если запущен хоть один QEventLoop, события обрабатываться будут нормально, потому в данном случае вызывать QApplication::processEvents() смысла нет. Я так понимаю когда мы делаем что-нибудь вроде QDialog::exec() — применяется похожий подход.


              1. ArmanPrestige
                12.11.2015 10:16

                Почему извращение? Обработка событий, на то и обработка событий, чтобы события обрабатывались… Если вы сразу этого не поняли, значит не до конца понимаете как устроены события в Qt.

                З.Ы. Насчёт msleep — я думал мы с товарищем midday рассматривали метод sendRequest. Если говорить о sendWhileSuccess — соглашусь, не совсем корректно его (msleep) тут использовать. Лучше заменить его на… QEventLoop.


                1. Zifix
                  12.11.2015 10:37
                  +1

                  Обработка событий, на то и обработка событий, чтобы события обрабатывались…
                  Спасибо за информацию.

                  Если вы сразу этого не поняли, значит не до конца понимаете как устроены события в Qt.
                  Тут вы правы, мне еще учиться и учиться. Только вот согласно документации, работа с QNetworkAccessManager должна быть построена с использованием механизма слотов и сигналов, как и вся остальная система событий в Qt.

                  Насчёт msleep — я думал мы с товарищем midday рассматривали метод sendRequest.
                  Заблудились в двух собственных методах? Печально.

                  Лучше заменить его на… QEventLoop.
                  Хорошо хоть вы до конца понимаете как устроены события в Qt, и просветили нас новеллой с тремя вложенными QEventLoop в программе с тремя классами.


                  1. ArmanPrestige
                    12.11.2015 11:15

                    Заблудились в двух собственных методах? Печально.

                    Сарказмщик. Сформулирую инчае — я думал, речь идёт об одном методе, а не о двух.

                    с тремя вложенными QEventLoop в программе с тремя классами.

                    По вашей логике, вызов QDialog::exec — не самая лучшая идея, так как фактически получается цикл обработки в цикле обработке, то есть два цикла вложенные друг в друга.


                  1. midday
                    12.11.2015 12:06
                    +1

                    Ага. Да туплю. QEventLoop не фризит. Но слип нужно заменить. Хотя все это противоречит всей идеалогии.


  1. savostin
    11.11.2015 19:48

    Google дает более интересную мысль с QNetworkReply::downloadProcess