UDP Networking with QSocketDevice
The QSocketDevice class provides a low-level interface that can be used for TCP and for UDP. For most TCP applications, the higher-level QSocket class is all we need, but if we want to use UDP, we must use QSocketDevice directly.
UDP is an unreliable, datagram-oriented protocol. Some application-level protocols use UDP because it is more lightweight than TCP. With UDP, data is sent as packets (datagrams) from one host to another. There is no concept of connection, and if a UDP packet doesn't get delivered successfully, no error is reported to the system.
Figure 13.3. The Weather Station application
We will see how to use UDP from a Qt application through the Weather Balloon and Weather Station example. The Weather Balloon application is a non-GUI application that sends a UDP datagram containing the current atmospheric conditions every 5 seconds. The Weather Station application receives these datagrams and displays them on screen. We will start by reviewing the code for the 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; };
The WeatherBalloon class inherits from QPushButton. It uses its QSocketDevice private variable for communicating with the Weather Station.
WeatherBalloon::WeatherBalloon(QWidget *parent, const char *name) : QPushButton(tr("Quit"), parent, name), socketDevice(QSocketDevice::Datagram) { socketDevice.setBlocking(false); myTimerId = startTimer(5 * 1000); }
In the constructor's initialization list, we pass QSocketDevice::Datagram to the QSocketDevice constructor to create a UDP socket device. In the constructor body, we call setBlocking (false) to make the QSocketDevice asynchronous. (By default, QSocketDevice is synchronous.)
We call startTimer() to generate a timer event every 5 seconds.
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); } }
In the timer event handler, we generate a datagram containing the current date, time, temperature, humidity, and altitude:
QDateTime |
Date and time of measurement |
double |
Temperature (in °C) |
double |
Humidity (in %) |
double |
Altitude (in meters) |
The datagram is sent using writeBlock(). The third and fourth arguments to writeBlock() are the IP address and the port number of the peer (the Weather Station). For this example, we assume that the Weather Station is running on the same machine as the Weather Balloon, so we use an IP address of 127.0.0.1 (0x7F000001), a special address that designates the local host. Unlike QSocket, QSocketDevice does not accept host names, only host numbers. If we wanted to resolve a host name to its IP address here, we would need to use the QDns class.
As usual, we need a main() function:
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(); }
The main() function simply creates a WeatherBalloon object, which serves both as a UDP peer and as a QPushButton on screen. By clicking the QPushButton, the user can quit the application.
Now let's review the source code for the 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; };
The WeatherStation class inherits from QDialog. It listens to a certain UDP port, parses any incoming datagrams (from the Weather Balloon), and displays their contents in five read-only QLineEdits.
The class has two private variables of interest here: socketDevice and socket-Notifier. The socketDevice variable, of type QSocketDevice, is used for reading datagrams. The socketNotifier variable, of type QSocketNotifier, is used to make the application aware of incoming datagrams.
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())); ... }
In the constructor's initialization list, we pass QSocketDevice::Datagram to the QSocketDevice constructor to create a UDP socket device. In the constructor body, we call setBlocking (false) to make the socket asynchronous and we call bind() to assign a port number to the socket. The first argument is the IP address of the Weather Station. By passing QHostAddress(), we indicate that we will accept datagrams to any IP address that belongs to the machine the Weather Station is running on. The second argument is the port number.
Then we create a QSocketNotifier object to monitor the socket. The QSocketNotifier will emit an activated(int) signal whenever the socket receives a datagram. We connect that signal to our dataReceived() slot.
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)); }
In dataReceived(), we call readBlock() on the QSocketDevice to read in the datagram. QByteArray::data() returns a pointer to the QByteArray's data, which readBlock() populates. Then, we extract the different fields using a QDataStream, and we update the user interface to show the information we received. From the application's point of view, datagrams are always sent and received as a single unit of data. This means that if any bytes are available, then exactly one datagram has arrived and can be read.
int main(int argc, char *argv[]) { QApplication app(argc, argv); WeatherStation station; app.setMainWidget(&station); station.show(); return app.exec(); }
Finally, in main(), we create a WeatherStation and make it the application's main widget.
We have now finished our UDP sender and receiver. The applications are as simple as possible, with the Weather Balloon sending datagrams and the Weather Station receiving them. In most real-world applications, both applications would need to both read and write on their socket. The QSocketDevice class has a peerAddress() and a peerPort() function that can be used by the server to determine what address and port to reply to.