Взаимодействие с веб-сервером в Qt

Вместо эпиграфа

Эта статья открывает цикл с рабочим названием «Знание в массы». В рамках данного цикла программисты, когда-либо писавшие на Qt, будут делиться своим опытом в решении простых и не очень проблем, с которыми им пришлось столкнуться. В статьях из этого цикла вы не найдете описания внутреннего устройства системных функций библиотеки и исчерпывающего анализа всех возможных подходов к решению той или иной проблемы. Вместо этого, будет поставлена одна, но типовая и часто встречающаяся задача, и предложено одно, но стопроцентно рабочее и проверенное на практике решение. Почему выбран именно такой подход? Дело в том, что когда я предлагал некоторым своим коллегам, знакомым с Qt, опубликовать статью на этом ресурсе, большинство отказывалось, мотивируя тем, что им нечего рассказать, что они не обладают никакими уникальными знаниями о библиотеке. Я с такой позицией не согласен, поэтому и решил предложить формат, в котором практически любой человек, знакомый с Qt, мог бы поделиться своими знаниями. Кроме того, не секрет, что если есть несколько способов решения одной задачи, то в подавляющем большинстве случаев подходит любой из них, независимо от времени работы и вычислительной сложности. И хотя все мы хотим, чтобы программа расходовала минимальное число ресурсов, часто бывает, что затраты на оптимизацию кода не оправдываются полученным в результате выигрышем. Таким образом, целей две.

 

  1. Предложить молодым программистом площадку для обмена опытом и мотивировать их для написания статей.
  2. Создать на qt.e-werest.org базу типовых задач и их решений, которыми мог бы пользоваться любой участник сообщества. Причем автором будет не неизвестный человек на незнакомом форуме, а ваш коллега и, возможно, сотрудник профильной лаборатории, следовательно, его решениям в большинстве случаев можно доверять.

А теперь, когда я окончательно утомил читателя непропорционально длинным вступлением, приступим непосредственно к статье.

Введение

Довольно часто на практике возникает задача организации взаимодействия между несколькими устройствами посредством сети интернет. Причем устройства могут отличаться друг от друга практически всем: операционной системой, архитектурой, наличием/отсутствием модулей связи, и даже типом (телефон, планшет, ноутбук, …). В таком случае для не нагруженных приложений идеально подходит архитектура “клиент-сервер”. Для каждого устройства пишется свой клиент на родном языке программирования и с учетом особенностей платформы, а роль связующего звена выполняет веб-сервер (см. рис. 1). Рисунок 1. Возможная схема взаимодействия устройств через веб-сервер В данной статье будет рассмотрена задача связи мобильного приложения с web-сервером по протоколу HTTP, описан механизм отправки запроса, получения ответа и анализа принятых данных. Рассказ построен на опыте написания клиента для многопользовательской игры “Красное и черное”. Правила и онлайн-версия доступны на сайте http://igornaya-zona.ru. Предполагается, что читатель уже ознакомился с несколькими предыдущими статьями или уже имеет опыт создания приложений с графическим интерфейсом, поэтому вопросы, связанные с GUI, в данной работе не рассматриваются. В таком случае появляется 3 относительно независимые задачи:

  • получение от сервера актуальных данных о ходе игры (сделанные ставки, сумма в банке, оставшееся до конца раунда время и т. п.);
  • синтаксический анализ полученного ответа и обновление значений соответствующих переменных;
  • отправка сообщения на сервер в случае, если пользователь сделал ставку.

Рассмотрим их по отдельности.

Получение данных от сервера

Поскольку минимальным временным интервалом в игре является 1 секунда, нет необходимости опрашивать сервер чаще. Кроме того, нельзя опрашивать его и реже, иначе может возникнуть ситуация, в которой часы нескольких клиентов рассинхронизированы. Следовательно, можно не писать избыточный код с асинхронным взаимодействием со стороны сервера при наступлении определенного события (например, ставки) а просто посылать запросы с клиентов раз в секунду. Здесь пригодится стандартный таймер Qt - QTimer. Пусть в заголовочном файле объявлена переменная timer типа QTimer*. Для начала в конструкторе окна ее необходимо инициализировать. Затем следует создать слот getServerInfo() и связать его с сигналом таймера timeout(). Последней инструкцией таймер запускается с интервалом в 1 секунду (1000 миллисекунд).


timer = new QTimer(this);
QObject::connect(timer, SIGNAL(timeout()), this, SLOT(getServerInfo()));
timer-gt;start(1000);

Далее рассмотрим код функции getServerInfo()


void MainWindow::getServerInfo()
{
const QNetworkRequest request(QUrl(QString::\
    fromAscii("http://igornaya-zona.ru/check.php?uid=vit")));
nam.post(request, &buffer);
}

В ней выполняется 2 операции:

 

  • с помощью QNetworkRequest формируется запрос с указанием имени клиента (это необходимо для того, чтобы получить актуальную информацию о сумме на личном счете);
  • с помощью объекта nam класса QNetworkAccessManager сформированный POST запрос отсылается на сервер.

Казалось бы, код для обработки ответа должен также быть в теле этой функции, однако не все так просто. Дело в том, что при плохом соединении или высокой загрузке сервера запрос, а тем более ответ, может идти довольно долго, поэтому разработчики Qt применили схему асинхронного взаимодействия. В конструкторе окна сигнал finished() объекта nam соединяется со слотом replyFinished() объекта окна.



QObject::connect(&nam, SIGNAL(finished(QNetworkReply*)), \
    this, SLOT(replyFinished(QNetworkReply*)));

Таким образом, сразу после отправки запроса управление снова передается обработчику системных сообщений, и программа не зависает, а при получении ответа будет вызвана функция replyFinished(). То есть и таймер, и функция посылки запросов, как бы скрыты от глаз того, кто пишет обработчик ответов. Для него вызов функции replyFinished() при получении ответа ничем не отличается от вызова функции buttonClicked() при нажатии кнопки на экране. Алгоритм обработчика ответов будет рассмотрен в следующем разделе.

 

Синтаксический анализ ответа

Одним из наиболее удобных форматов для передачи ответов от сервера является XML. В случае его использования практически на любой платформе отпадает необходимость писать свой parser - почти везде есть стандартные. Кроме того, в нашем случае достаточно самого простого - одноуровневого - синтаксиса. Ниже приведен пример ответа:



7100000164615

Способ анализа, предложенный ниже, отличается простотой, однако содержит в себе, как минимум, одно лишнее копирование. Если кто-то знает решение лучше, опишите его в комментариях. Поскольку максимальная длина ответа не зависит от числа игроков и заранее известна, для его хранения можно создать статический буфер. Сначала ответ считывается в массив data из объекта reply, а затем полученные данные передаются для анализа в объект reader класса QXmlStreamReader. Далее запускается цикл, который поэлементно (опираясь на теги) считывает данные из буфера. На каждом шаге присутствует проверка, является ли текущий элемент открывающимся тегом. Если да, то его имя имя можно получить, вызвав функцию reader.name(), а текст, заключенный между открывающим и закрывающим тегами - вызвав reader.readElementText(). Теперь в зависимости от вида тега будет обновлено значение соответствующей переменной состояния, и на этом синтаксический анализ ответа завершен. Замечу лишь, что порядок if’ов совершенно не важен и что предложенный метод хорошо работает только с XML, не содержащим вложенных тегов. Уже для двухуровневых файлов лучше применять другие способы (например, описанный в книге М. Шлее “Qt. Профессиональное программирование на C++”, глава 39), иначе придется писать вложенные циклы. Ниже представлен код функции, реализующей описанный функционал:



void MainWindow::replyFinished(QNetworkReply* reply)
{
char data[1000];
reply-gt;
read(data, 999);
QXmlStreamReader reader(data);
while (!reader.atEnd())
{
reader.readNext();
if (reader.isStartElement())
{ if (reader.name() == "balance")
{
m_money_num = reader.readElementText().toInt();
}
else if (reader.name() == "red")
{
m_area_red_num = reader.readElementText().toInt(); 
}
else if (reader.name() == "black")
{
m_area_black_num = reader.readElementText().toInt();
}
else if (reader.name() == "myred")
{
m_area_my_red_num = reader.readElementText().toInt();
}
else if (reader.name() == "myblack")
{
m_area_my_black_num = reader.readElementText().toInt();
}
else if (reader.name() == "win")
{
m_win = true;
}
else if (reader.name() == "timeleft")
{
m_time_num = reader.readElementText().toInt();
}
else
{
qDebug() << "The file is not an XBEL version 1.0 file.";
}
}
}
//обновление значений элементов интерфейса в соответствии \
//с новыми значениями переменных состояния (не рассматривается)
//...
//...
}

Последним по списку, но не по значению, идет описание алгоритма отправки информации о действиях пользователя на сервер.

 

Отправка сообщений с указанием ставки

Единственным действием, которое может выполнять игрок, является ставка: одна, две или пять монет либо на красное, либо на черное. Причем пользователь может ставить и на красное, и на черное, и по несколько раз: он ограничен в своих действиях только размером кошелька. Одним из примеров выигрышной стратегии является “ловушка”. Игрок ставит на красное поле, например, 10 монет. Все остальные ставят на черное поле суммарно 9 монет и ждут выигрыша, но за пару секунд до окончания раунда игрок ставит на черное две монеты. В результате, он теряет эти две монеты, но выигрывает 10, таким образом, чистая прибыль составляет 8 монет. Поскольку игрок может только делать ставки, все запросы будут одного типа и разумно использовать некий шаблон. Кроме того, на эти запросы не нужно ждать ответов, ведь от сервера и так каждую секунду приходят сообщения с текущими данными по игре. Таким образом, достаточно создать одну функцию add(), и вызывать ее из всех обработчиков с соответствующими параметрами: цвет и размер ставки. Рассмотрим подробнее функцию add(). Проверка корректности данных на клиенте является излишней, так как сервер все равно проверяет все запросы перед выполнением (защита от мошенничества). Поэтому необходимо только создать PUT запрос (такой тип является стандартным для обновления информации на сервере) и отправить его, то есть совершить действия, аналогичные описанным в разделе “Получение данных от сервера”. Ниже приведен полный код функции.



void MainWindow::add(int type, int number)
{
char data[1000];
sprintf(data, "%s&field=%s&amount=%d", \
    "http://igornaya-zona.ru/put.php?uid=vit", \
    (type==RED)?"red":"black", number);
const QNetworkRequest request(QUrl(QString::fromAscii(data)));
QNetworkRequest \
    req(QUrl("http://igornaya-zona.ru/check.php?uid=vit"));
nam.put(request, &buffer);
}

А вот так выглядит типовой ее вызов из обработчика.



void MainWindow::buttonAddRed2Clicked()
{
qDebug() << "Add 2 to red field";
add(RED, 2);
}

 

Заключение

Как видите, работа из приложения на Qt с веб-сервером организуется очень просто, что и было показано в данной статье. Еще раз повторюсь, я не ставил целью описать все возможные способы организации взаимодействия, а выбрал один, может и не самый лучший, но очень простой и неоднократно проверенный на практике. С нетерпением жду вопросов и комментариев, как по формату, так и по сути статьи. Если у вас есть какие-либо замечания или предложения, пожалуйста, оставляйте их на ресурсе или отправляйте письмом на адрес vit.petrov@vu.spb.ru. Виталий Петров