Робота з HTTP у Qt

5 хв. читання

В інтернеті є багато статей, які описують роботу з протоколом HTTP у Qt, але більшість з них дуже поверхневі (навіть HTTP Example з офіційної документації). Мені б хотілося більше розказати про деякі нюанси, що можуть виникнути при розробці клієнта для якого-небудь HTTP API.

GET

Робота з http у Qt 5 проводиться за допомогою класу QNetworkAccessManager. Базове відправлення GET-запиту виглядає дуже просто:

  nam = new QNetworkAccessManager(this);
  connect(nam, SIGNAL(finished(QNetworkReply*)),
          this, SLOT(processSimpeGetFinished(QNetworkReply*)));

  nam->get(QNetworkRequest(QUrl("http://doc.qt.io/")));

Слот, приєднаний до сигналу finished, отримає всю інформацію, що надійшла від сервера. Обробка відповіді може виглядати приблизно так:

void FoxtrotPenguin::processSimpleParamGetFinished(QNetworkReply *reply) {
  if (reply->error() != QNetworkReply::NoError) {
    qDebug() << "Got some error " << reply->error();
    QCoreApplication::exit(1);
  }

  const int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
  qDebug() << "Received code " << resultCode;
  if (resultCode != 200) {
    QCoreApplication::exit(1);
  }

  writeFile(".\\\\spGetResult.html", reply->readAll());
  QCoreApplication::exit(0);
}

Самі дані відповіді можна взяти методом reply->readAll(), але до того варто зробити пару стандартних дій:

  • перевірити метод error() - спрацює, якщо виникнуть проблеми з мережею, або якщо ви неправильно вказали адресу сайту
  • перевірити код стану HTTP.
  • розпакувати дані, якщо застосовувалось стиснення. Докладніше про це буде сказано нижче.

Замість QNetworkAccessManager::finished() можна використовувати сигнали readyRead() та finished() класу QNetworkReply. Тоді відправка запиту виглядатиме так:

  nam = new QNetworkAccessManager(this);

  currentReply = nam->get(QNetworkRequest(QUrl("http://doc.qt.io/")));

  connect(currentReply, SIGNAL(finished()), this, SLOT(processLongGetFinished()));
  connect(currentReply, SIGNAL(readyRead()), this, SLOT(processLongGetReadyRead()));

Очевидно, такий спосіб краще підходить для отримання великих обсягів даних (наприклад, завантаження файлів). Незначний недолік полягає у тому, що він потребує окремого члену класу для зберігання вказівника на об'єкт QNetworkReply.

Сигнал readyRead з'явиться кілька разів по мірі надходження даних, а finished обов'язково спрацює після завершення відповіді. Обробка сигналу _finished_ виглядатиме майже так само, лише в кінці треба буде подбати про видалення опрацьованого об'єкту QNetworkReply. У документації сказано, що його бажано видаляти не прямо, а методом deleteLater(), як це зроблено у HTTP Example:

    reply->deleteLater();
    reply = Q_NULLPTR;

GET з параметрами

В принципі, усі параметри GET можна покласти у тому ж самому рядку, який передається QUrl. Наприклад, ось так можна отримати результат голосування 9711 у Верховній Раді:

nam->get(QNetworkRequest(QUrl("http://w1.c1.rada.gov.ua/pls/radan_gs09/ns_golos?g_id=9711")));

Але може виникнути проблема, якщо параметри містять кирилицю або інші символи, що потребують додаткового кодування. Наприклад, валідатор W3c потребує повну адресу сайту, який треба перевірити, щось типу "https://validator.w3.org/nu/?doc=http%3A%2F%2Frada.gov.ua%2F". Для того, щоб коректно це обробити, краще сформувати QUrl за допомогою QUrlQuery

  const QString endpoint = "https://validator.w3.org/nu/" ;  
  QUrl url(endpoint);

  QUrlQuery query ;
  query.addQueryItem("doc", "http://rada.gov.ua/");
  url.setQuery(query);

  QNetworkRequest req( url ) ;
  nam->get( req );

POST

Запити POST відправляються так само, як і GET, але при відправленні веб-форми виникає декілька моментів, на які треба звернути увагу.

По-перше, треба додати до запиту заголовок "application/x-www-form-urlencoded" і вказати довжину даних.

По-друге, знову виникає проблема з кодуванням кирилічних символів; в цьому випадку вона ще глибша. Потрібно спочатку використати QUrlQuery для формування і кодування даних форми, а потім замінити символи "+" на "%2B"

Таким чином, надсилання веб-форми виглядатиме так (на прикладі пошуку справ на сайті апеляційного суду міста Києва):

nam = new QNetworkAccessManager(this);

  connect(nam, SIGNAL(finished(QNetworkReply*)),
          this, SLOT(processPostFormFinished(QNetworkReply*)));

  const QString endpoint = "http://www.apcourtkiev.gov.ua/CourtPortal.WebSite/Home/GraficZasidan";
  QUrl url(endpoint);

  QUrlQuery query ;
  query.addQueryItem("case_number", "");
  query.addQueryItem("session_date", QDateTime::currentDateTime().toString("yyyy-MM-dd"));
  query.addQueryItem("main_judge", "");
  query.addQueryItem("litigant", "");
  query.addQueryItem("doc_class", "");
  query.addQueryItem("Search", "Шукати");

  QString qs = query.query(QUrl::FullyEncoded) ;
  qs.replace("+", "%2B");

  QByteArray qsba = qs.toUtf8();

  QNetworkRequest nrq(url);
  nrq.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
  nrq.setHeader(QNetworkRequest::ContentLengthHeader, qsba.count());

  nam->post( nrq, qsba);

Для англомовних сайтів проблема кодування не так важлива, тому у більшості прикладів радять просто ліпити зміст форми стандартними методами QByteArray.

Cookies

З куками все просто: для них існує спеціальний клас QNetworkCookieJar. За замовчанням він не використовується, об'єкт треба явно створити і передати до QNetworkAccessManager:

  networkAccessManager->setCookieJar(new QNetworkCookieJar());

Після цього в усіх запитах, виконаних за допомогою цього networkAccessManager, куки зберігатимуться та надсилатимуться автоматично. Також їх можна спробувати отримати вручну, як і будь-який інший заголовок:

QVariant cookieHeader = reply->header(QNetworkRequest::SetCookieHeader);

HTTPS

QNetworkAccessManager сам розбирається, коли треба використовувати https і не потребує ніяких додаткових дій.

Єдиний момент - у деяких випадках скомпільована під Windows програма може почати писати у консоль повідомлення про помилки такого роду:

qt.network.ssl: QSslSocket: cannot resolve SSL_set_psk_client_callback
qt.network.ssl: QSslSocket: cannot resolve TLSv1_1_client_method
qt.network.ssl: QSslSocket: cannot resolve TLSv1_2_client_method
qt.network.ssl: QSslSocket: cannot resolve TLSv1_1_server_method
qt.network.ssl: QSslSocket: cannot resolve TLSv1_2_server_method
qt.network.ssl: QSslSocket: cannot resolve SSL_select_next_proto
qt.network.ssl: QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb
qt.network.ssl: QSslSocket: cannot resolve SSL_get0_next_proto_negotiated

Це якісь проблеми компіляції бібліотек Qt та ліцензування. По суті можна не звертати уваги, бо на роботу воно не впливає.

Стиснення

Багато сайтів використовують стиснення ("gzip") відповіді. Щоб його коректно обробити, треба до запиту додати відповідний заголовок

 QNetworkRequest nrq(url);
  nrq.setRawHeader("Accept-Encoding","gzip, deflate");

Потім перевірити заголовки відповіді:

  bool isGzipped = false;
  QList<qnetworkreply::rawheaderpair> rhplist = reply->rawHeaderPairs();  
  QList<qnetworkreply::rawheaderpair>::const_iterator rhpi = rhplist.constBegin() ;
  for (;rhpi!=rhplist.constEnd(); rhpi++) {
    if ((rhpi->first=="Content-Encoding")&&(rhpi->second=="gzip")) {
      isGzipped = true ;
    }
  }  

Саме розпаковування виконується за домомогою бібліотеки zlib, завершений приклад коду, який його робить, можна взяти тут. Бібліотеку zlib1.dll доведеться тримати разом з виконуваним файлом, статична компіляція не передбачена.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.2K
Приєднався: 9 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація