13.4. Протокол UDP и класс QSocketDevice.

Класс QSocketDevice реализует низкоуровневый интерфейс для работы с протоколами UDP и TCP. В большинстве TCP-приложений, достаточно будет функциональности, заложенной в QSocket, но если в приложении необходимо работать с протоколом UDP, то тогда вам придется обратить свой взор на класс QSocketDevice.

UDP -- это протокол с негарантированной доставкой сообщений, ориентированный на работу с датаграммами. Некоторые нестандартные протоколы работают поверх UDP, поскольку он не такой "тяжелый" как TCP. По протоколу UDP, данные передаются в виде отдельных пакетов - датаграмм. В этом протоколе отсутствует понятие "соединения", если UDP-пакет теряется где-то в недрах сети, то система не получит уведомления об ошибке.

Рисунок 13.3. Внешний вид приложения TheWeather Station.


Рассмотрим принципы организации сетевых взаимодействий по протоколу UDP, на примере приложений Weather Balloon и Weather Station. Приложение Weather Balloon не имеет графического интерфейса. Его задача -- раз в 5 секунд отсылать UDP-датаграмму, которая содержит информацию о погодных условиях. Приложение Weather Station будет принимать эти датаграммы и отображать полученные сведения на экране. Начнем с приложения Weather Balloon. class WeatherBalloon : public QPushButton { Q_OBJECT public: WeatherBalloon(QWidget *parent = 0, const char *name = 0); double temperature() const; double humidity() const; double altitude() const; protected: void timerEvent(QTimerEvent *event); private: QSocketDevice socketDevice; int myTimerId; }; Класс WeatherBalloon порожден от QPushButton. Для взаимодествия с Weather Station он использует QSocketDevice. WeatherBalloon::WeatherBalloon(QWidget *parent, const char *name) : QPushButton(tr("Quit"), parent, name), socketDevice(QSocketDevice::Datagram) { socketDevice.setBlocking(false); myTimerId = startTimer(5 * 1000); } В списке инициализаторов конструктора, вызовом QSocketDevice::Datagram, создается устройство QSocketDevice. В теле конструктора, созданное устройство переводится в асинхронный режим работы, вызовом setBlocking(false). (По-умолчанию, QSocketDevice работают в синхронном режиме.) void WeatherBalloon::timerEvent(QTimerEvent *event) { if (event->timerId() == myTimerId) { QByteArray datagram; QDataStream out(datagram, IO_WriteOnly); out.setVersion(5); out << QDateTime::currentDateTime() << temperature() << humidity() << altitude(); socketDevice.writeBlock(datagram, datagram.size(), 0x7F000001, 5824); } else { QPushButton::timerEvent(event); } } В обработчике событий от таймера генерируется датаграмма, содержащая текущие дату, время, температуру воздуха, влажность и высоту над уровнем моря:
QDateTime Дата и время измерений
double Температура (в градусах Цельсия)
double Влажность (в %)
double Высота над уровнем моря (в метрах)
Передача датаграммы осуществляется вызовом writeBlock(). Третий и четвертый аргументы функции writeBlock() -- это IP-адрес и номер порта клиентского узла (Weather Station). В данном случае мы исходим из предположения, что оба приложения работают на одной машине, поэтому IP-адрес -- 127.0.0.1 (0x7F000001). В отличие от QSocket, экземпляры класса QSocketDevice не работает с сетевыми именами компьютеров, ему нужны IP-адреса. Если вам необходимо преобразовать имя хоста в его IP-адрес, это можно сделать с помощью класса QDns. int main(int argc, char *argv[]) { QApplication app(argc, argv); WeatherBalloon balloon; balloon.setCaption(QObject::tr("Weather Balloon")); app.setMainWidget(&balloon); QObject::connect(&balloon, SIGNAL(clicked()), &app, SLOT(quit())); balloon.show(); return app.exec(); } Функция main() просто создает объект WeatherBalloon, который выступает в двух ипостасях: как UDP-узел и как кнопка QPushButton на экране.

Теперь перейдем к приложению Weather Station.

class WeatherStation : public QDialog { Q_OBJECT public: WeatherStation(QWidget *parent = 0, const char *name = 0); private slots: void dataReceived(); private: QSocketDevice socketDevice; QSocketNotifier *socketNotifier; QLabel *dateLabel; QLabel *timeLabel; ... QLineEdit *altitudeLineEdit; }; Класс WeatherStation порожден от QDialog. Его назначение -- ожидать поступления датаграмм на определенном порту, производить разбор поступившей информации (от Weather Balloon) и отображать ее в пяти компонентах QLineEdit.

Класс содержит две приватные переменные, которые представляют для нас наибольший интерес: socketDevice и socketNotifier. Первая переменная имеет тип QSocketDevice. Она используется для приема датаграмм. Вторая переменная имеет тип QSocketNotifier. Она используется для того, чтобы известить приложение о поступлении датаграммы.

WeatherStation::WeatherStation(QWidget *parent, const char *name) : QDialog(parent, name), socketDevice(QSocketDevice::Datagram) { socketDevice.setBlocking(false); socketDevice.bind(QHostAddress(), 5824); socketNotifier = new QSocketNotifier(socketDevice.socket(), QSocketNotifier::Read, this); connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(dataReceived())); ... } В списке инициализаторов конструктора создается устройство QSocketDevice, вызовом QSocketDevice::Datagram. В теле конструктора, вызовом setBlocking(false), оно переводится в асинхронный режим работы. Вызовом функции bind(), сокету назначаются IP-адрес и номер порта. Вызовом функции QHostAddress() мы сообщаем, что будем принимать датаграммы, отправляемые на любой IP-адрес, который принадлежит компьютеру с запущенным приложением Weather Station.

Затем создается объект QSocketNotifier, который будет "следить" за сокетом. Объект QSocketNotifier будет выдавать сигнал activated(int) в момент поступления датаграммы. Этот сигнал соединяется со слотом dataReceived().

void WeatherStation::dataReceived() { QDateTime dateTime; double temperature; double humidity; double altitude; QByteArray datagram(socketDevice.bytesAvailable()); socketDevice.readBlock(datagram.data(), datagram.size()); QDataStream in(datagram, IO_ReadOnly); in.setVersion(5); in >> dateTime >> temperature >> humidity >> altitude; dateLineEdit->setText(dateTime.date().toString()); timeLineEdit->setText(dateTime.time().toString()); temperatureLineEdit->setText(tr("%1 C").arg(temperature)); humidityLineEdit->setText(tr("%1%").arg(humidity)); altitudeLineEdit->setText(tr("%1 m").arg(altitude)); } В функции dataReceived() производится чтение датаграммы, вызовом readBlock(). Функция QByteArray::data() возвращает указатель на данные в QByteArray, по которому readBlock() запишет полученные сведения. Затем производится извлечение отдельных значений, которые заносятся в визуальные компоненты для отображения на экране. С точки зрения приложения, датаграмма всегда передается и принимается как единый блок данных. Это означает, что если доступен хотя бы один байт, то следовательно датаграмма получена целиком. int main(int argc, char *argv[]) { QApplication app(argc, argv); WeatherStation station; app.setMainWidget(&station); station.show(); return app.exec(); } В заключение, в функции main() создается объект WeatherStation и назначается главным виджетом приложения.

На этом мы заканчиваем рассмотрение принципов работы с UDP. Мы постарались создать приемное и передающее приложения настолько простыми, насколько это возможно. В большинстве реальных применений, приложения должны как передавать, так и принимать датаграммы. В составе класса QSocketDevice имеются функции peerAddress() и peerPort(), которые могут использоваться для определения IP-адреса и номера порта, на которые нужно послать ответ.