Мне нужен был простой интерфейс для отправки 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)
Laert
11.11.2015 14:47+1Рассмотри возможность вынести RequestSender в отдельный поток сделав менеджер его частью. Там же хранить сет реквестов и их id. Тайм ауты можно контролировать через QSignalMapper получаю в итоге id request который отвалился
savostin
11.11.2015 19:11Если не ошибаюсь, функции асинхронные, зачем в поток-то?
ArmanPrestige
11.11.2015 19:19Асинхронные. Одна из задач класса RequestSender — скрыть эту асинхронность.
Zifix
11.11.2015 19:26+1Работа с REST API в Qt, больная тема, конечно, приходится писать приличных размеров велосипеды — и все осложняется недостатком информации как правильно реализовывать нюансы этой работы.
Как уже писали выше, решение использовать QEventLoop выглядит странным, вследствии чего выполняется не менее странная проверка успешности выполнения в том же методе, без использования механизма сигналов/слотов. RequestError имеет всего два состояния, хватило бы возвращать bool. Хотя по сути ошибок может быть значительно больше, что в этой реализации не отлавливается в соответствующем слоте. Исключение как стандартный метод возврата результата в Qt — жуткий моветон, заставлять текущий (возможно главный) поток спать в бесконечном цикле, опять же без сигналов/слотов — быдлокод, и т.д.
Не хотелось бы отбить желание выкладывать код, но если честно, то Request выглядит не сильно полезнее стандартной связки QUrl + QUrlQuery, Network же с одной стороны примитивный, с другой стороны написан криво :/ArmanPrestige
11.11.2015 19:39RequestError имеет всего два состояния, хватило бы возвращать bool. Хотя по сути ошибок может быть значительно больше
Да, именно потому, что ошибок может быть больше, не возвращается bool. Для моих целей мне, пока что, хватило этих двух состояний. Однако, было бы неплохо обрабатывать и другие случаи.
заставлять текущий (возможно главный) поток спать в бесконечном цикле, опять же без сигналов/слотов — быдлокод, и т.д.
А какую реализацию предложите для «избавления» от асинхронности QNetworkAccessManager? Не сказал бы «спал», скорее ожидание события. Если бы вы использовали другую библиотеку, которая работает не асинхронно, то у вас и там главный поток «спал», пока не придёт ответ. Я пока ничего лучшего кроме как связки с QEventLoop-ом не придумал.Zifix
11.11.2015 19:43Я пока ничего лучшего кроме как связки с QEventLoop-ом не придумал.
Стандартный случай для этого — очередь. В связке с со слотом, подписанным на finished(), конечно.
Zifix
11.11.2015 20:52Upd:
Не сказал бы «спал», скорее ожидание события
И в это время он ничего не делает, верно? Спит в ожидании события, хорошо.Если бы вы использовали другую библиотеку, которая работает не асинхронно, то у вас и там главный поток «спал», пока не придёт ответ.
Давайте еще микроконтроллеры вспомним, ага. У нас ведь главный поток отвечает за отрисовку интерфейса пользователя, и если он спит пока идет запрос (максимум 2*n секунд) — UI не отвечает на внешние раздражители.ArmanPrestige
11.11.2015 20:59Давайте еще микроконтроллеры вспомним, ага. У нас ведь главный поток отвечает за отрисовку интерфейса пользователя, и если он спит пока идет запрос (максимум 2*n секунд) — UI не отвечает на внешние раздражители.
Никаких фризов не происходит, интерфейс отрисовывается и реагирует на все события. Благодаря использованию QEventLoop::exec.Zifix
11.11.2015 21:04Если мы вызываем sendWhileSuccess и у нас все maxCount раз неудачно проходит запрос — то никаких фризов нет?
ArmanPrestige
11.11.2015 21:31Нет.
Zifix
12.11.2015 06:28Что по-вашему фризы? Вот мы без перерыва кликаем, например, в чекбокс во время этого действа, и он мгновенно меняет свое состояние сколько бы мы не кликали?
midday
11.11.2015 21:45+1loop.exec(); Фризит в том то и дело. Он не отпустит пока не придет сигнал finished, это и есть способ сделать из асинхронной функции синхронную. А тем более вы вообще sleep потом вызываете.
Вы же сами писали «Одна из задач класса RequestSender — скрыть эту асинхронность.».
Чтобы гуи рисовались надо в циклах вызывать QApplication::processEvents(), можно там к примеру таймер завести 30 раз в секунду вызывать QApplication::processEvents().
А если честно, то я вообще не пойму поставленной задачи. QNetworkAccessManager простой как кирпич. Зачем все это объясните пожалуйста? Создал приемник сигналов от reply и радуешься. Хоть прогрессбары делай, хоть что… Отправил запрос и забыл. Ждешь в слоте.ArmanPrestige
11.11.2015 21:50-1Какой sleep? Никакой sleep не вызывается. Я ещё раз говорю, что не фризится гуи. Соберите и проверьте сами.
QNetworkAccessManager простой как кирпич.
Но не всегда удобный. Чтобы отправить один запрос, создавать QNetworkAccessManager, соединять сигналы со слотами (ещё и слот создавать), потом заботится об удалении QNetworkReply… Слишком много «движений» для отправки одного запроса. А мой класс всё это скрывает.Zifix
12.11.2015 06:05Какой sleep? Никакой sleep не вызывается.
QThread::currentThread()->msleep(2000);
Zifix
12.11.2015 07:15loop.exec(); Фризит в том то и дело...
Тут вы не правы, что не удивительно — с таким извращением встречаешься не каждый день. Очередь событий общая для потока, и если запущен хоть один QEventLoop, события обрабатываться будут нормально, потому в данном случае вызывать QApplication::processEvents() смысла нет. Я так понимаю когда мы делаем что-нибудь вроде QDialog::exec() — применяется похожий подход.ArmanPrestige
12.11.2015 10:16Почему извращение? Обработка событий, на то и обработка событий, чтобы события обрабатывались… Если вы сразу этого не поняли, значит не до конца понимаете как устроены события в Qt.
З.Ы. Насчёт msleep — я думал мы с товарищем midday рассматривали метод sendRequest. Если говорить о sendWhileSuccess — соглашусь, не совсем корректно его (msleep) тут использовать. Лучше заменить его на… QEventLoop.Zifix
12.11.2015 10:37+1Обработка событий, на то и обработка событий, чтобы события обрабатывались…
Спасибо за информацию.
Если вы сразу этого не поняли, значит не до конца понимаете как устроены события в Qt.
Тут вы правы, мне еще учиться и учиться. Только вот согласно документации, работа с QNetworkAccessManager должна быть построена с использованием механизма слотов и сигналов, как и вся остальная система событий в Qt.
Насчёт msleep — я думал мы с товарищем midday рассматривали метод sendRequest.
Заблудились в двух собственных методах? Печально.
Лучше заменить его на… QEventLoop.
Хорошо хоть вы до конца понимаете как устроены события в Qt, и просветили нас новеллой с тремя вложенными QEventLoop в программе с тремя классами.ArmanPrestige
12.11.2015 11:15Заблудились в двух собственных методах? Печально.
Сарказмщик. Сформулирую инчае — я думал, речь идёт об одном методе, а не о двух.
с тремя вложенными QEventLoop в программе с тремя классами.
По вашей логике, вызов QDialog::exec — не самая лучшая идея, так как фактически получается цикл обработки в цикле обработке, то есть два цикла вложенные друг в друга.
midday
12.11.2015 12:06+1Ага. Да туплю. QEventLoop не фризит. Но слип нужно заменить. Хотя все это противоречит всей идеалогии.
Laert
из документации советуют использовать один QNetworkAccessManager для всего приложения, как вариант вынести твой RequestSender в отдельный поток, а тайм ауты сделать частью
Laert
рубанули эликтричество =( у нас тут с этим плохо
ArmanPrestige
Не соглашусь с использованием одного QNetworkAccessManager-а на всё приложение. Бывают случаи, когда нужно отправлять запросы через прокси, причём прокси не один, а несколько. Тогда я создаю несколько RequestSender-ов и отправляю запросы с использованием прокси в разных потоках.
Laert
Ну раз зашла речь то неплохо бы еще тебе проверять редиректы вроде такого