[Из песочницы] Пишем Rest API клиент на Qt5
Введение
Последнее время я занимаюсь разработкой настольного Rest API клиента. Довольно большая часть работы во взаимодействие с сервером, для оптимизации обработки запросов был написан класс Requester, обладающий следующими особенностями:
- возможность отправлять как https так и http запросы
- использование одной функции для всех типов запросов
- возможность получить все данные по запросу с сервера, а не одну страницу (n записей)
Программисту, использующему этот класс, придется работать тремя функциями:
void initRequester(const QString& host, int port, QSslConfiguration *value);
// функция посылает один запрос
void sendRequest(const QString &apiStr,
const handleFunc &funcSuccess,
const handleFunc &funcError,
Type type = Type::GET,
const QVariantMap &data = QVariantMap());
//функция будет посылать GET запрос пока не будет достигнута последня старница
void sendMulishGetRequest(
const QString &apiStr,
const handleFunc &funcSuccess,
const handleFunc &funcError,
const finishFunc &funcFinish);
funcSuccess — callback, вызываемый в случае, если запрос выполнился успешно
funcError — callback в случае ошибки
typedef std::function handleFunc;
typedef std::function finishFunc;
enum class Type {
POST,
GET,
PATCH,
DELET
};
DELET — не опечатка, так как с DELETE не собирается под WINDOWS.
Реализация
Для взаимодействия с сервером будем использовать три Qt класса: QNetworkAccessManager — реализует механизм запросов на сервер, QNetworkReply — ответ сервера на наш запрос и QNetworkRequest — собственно сам запрос.
Я не вижу смысла описывать реализацию функции initRequester, поэтому перейдём сразу к SendRequest. Идея состоит в том, что мы создаем объект класса QNetworkRequest. В зависимости от типа запроса, передаем его с дополнительными данными (тело запроса, если есть) в объект класса QNetworkAccessManager. Ответ записывается в reply (объект класса QNetworkReply). Так как запросы выполняются асинхронно, то по сигналу finished от reply будем вызывать лямбду, проверяющую были ли ошибки и вызывающую соответсвующий callback, она же занимается высвобождением ресурсов.
Для создания request`а используется следующий код:
QNetworkRequest Requester::createRequest(const QString &apiStr)
{
QNetworkRequest request;
QString url = pathTemplate.arg(host).arg(port).arg(apiStr);
request.setUrl(QUrl(url));
request.setRawHeader("Content-Type","application/json");
// здесь прописываются все необходимые заголовки запроса
if (sslConfig != nullptr)
request.setSslConfiguration(*sslConfig);
return request;
}
А вот и код самой функции для запросов на сервер:
void Requester::sendRequest(const QString &apiStr,
const handleFunc &funcSuccess,
const handleFunc &funcError,
Requester::Type type,
const QVariantMap &data)
{
QNetworkRequest request = createRequest(apiStr);
QNetworkReply *reply;
switch (type) {
case Type::POST: {
QByteArray postDataByteArray = variantMapToJson(data);
reply = manager->post(request, postDataByteArray);
break;
} case Type::GET: {
reply = manager->get(request);
break;
} case Type::DELET: {
if (data.isEmpty())
reply = manager->deleteResource(request);
else
reply = sendCustomRequest(manager, request, "DELETE", data); //реализация ниже
break;
} case Type::PATCH: {
reply = sendCustomRequest(manager, request, "PATCH", data);
break;
} default:
reply = nullptr;
}
connect(reply, &QNetworkReply::finished, this, [this, funcSuccess, funcError, reply]() {
// данная часть функции написана с учетом того, что ответ будет в формате json
QJsonObject obj = parseReply(reply);
if (onFinishRequest(reply)) {
if (funcSuccess != nullptr)
funcSuccess(obj);
} else {
if (funcError != nullptr) {
handleQtNetworkErrors(reply, obj);
funcError(obj);
}
}
reply->close();
reply->deleteLater();
} );
}
Внимательный читатель заметил, что для некоторых запросов DELETE и всех PATCH для создания объекта QNetworkReply используется функция sendCustomRequest. Это объясняется тем, что QNetworkAccessManager не умеет из коробки посылать DELETE запросы с телом, и совсем не умеет PATCH. Для решения этой проблемы напишем небольшую функцию обертку:
QNetworkReply* Requester::sendCustomRequest(QNetworkAccessManager* manager,
QNetworkRequest &request,
const QString &type,
const QVariantMap &data)
{
request.setRawHeader("HTTP", type.toUtf8());
QByteArray postDataByteArray = variantMapToJson(data);
QBuffer *buff = new QBuffer;
buff->setData(postDataByteArray);
buff->open(QIODevice::ReadOnly);
QNetworkReply* reply = manager->sendCustomRequest(request, type.toUtf8(), buff);
buff->setParent(reply);
return reply;
}
Подробно рассматривать функцию sendMulishGetRequest не вижу смысла, так как она похожа на рассмотренную выше. Отличия заключаются в том, что после первого успешного выполнения запроса, из ответа будет вытащена ссылка на следующую страницу, после чего произойдет рекурсивный вызов. Если следующей страницы нет, то будет выполнена функция funcFinish.
Заключение
Получившийся в итоге, класс имеет очень простой интерфейс для использования, а так же избавляет нас от многих рутинных действий.