TCP Networking with QSocket
The QSocket class can be used to implement TCP clients and servers. TCP is a transport protocol that forms the basis of many application-level Internet protocols, including FTP and HTTP, and that can also be used for custom protocols.
TCP is a stream-oriented protocol. For applications, the data appears to be a long stream, rather like a large flat file. The high-level protocols built on top of TCP are typically either line-oriented or block-oriented:
- Line-oriented protocols transfer data as lines of text, each terminated by a newline.
- Block-oriented protocols transfer data as binary data blocks. Each block consists of a size field followed by size bytes of data.
QSocket inherits from QIODevice, so it can be read from and written to using a QDataStream or a QTextStream. One notable difference when reading data from a network compared with reading from a file is that we must make sure that we have received enough data from the peer before we use the >> operator. Failing to do so may result in undefined behavior.
In this section, we will review the code of a client and a server that use a custom block-oriented protocol. The client is called Trip Planner and allows users to plan their next train trip. The server is called Trip Server and provides the trip information to the client. We will start by writing the Trip Planner application.
Figure 13.1. The Trip Planner application
The Trip Planner provides a From field, a To field, a Date field, an Approximate Time field, and two radio buttons to select whether the approximate time is that of departure or arrival. When the user clicks Search, the application sends a request to the server, which responds with a list of train trips that match the user's criteria. The list is shown in a QListView in the Trip Planner window. The very bottom of the window is occupied by a QLabel that shows the status of the last operation and a QProgressBar.
The Trip Planner's user interface was created using Qt Designer. Here, we will focus on the source code in the corresponding .ui.h file. Note that the following four variables were declared in Qt Designer's Members tab:
QSocket socket; QTimer connectionTimer; QTimer progressBarTimer; Q_UINT16 blockSize;
The socket variable of type QSocket encapsulates the TCP connection. The connectionTimer variable is used to time out a connection that lasts too long. The progressBarTimer variable is used to refresh the progress bar periodically when the application is busy. Finally, the blockSize variable is used when parsing the blocks received from the server.
void TripPlanner::init() { connect(&socket, SIGNAL(connected()), this, SLOT(sendRequest())); connect(&socket, SIGNAL(connectionClosed()), this, SLOT(connectionClosedByServer())); connect(&socket, SIGNAL(readyRead()), this, SLOT(updateListView())); connect(&socket, SIGNAL(error(int)), this, SLOT(error(int))); connect(&connectionTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); connect(&progressBarTimer, SIGNAL(timeout()), this, SLOT(advanceProgressBar())); QDateTime dateTime = QDateTime::currentDateTime(); dateEdit->setDate(dateTime.date()); timeEdit->setTime(QTime(dateTime.time().hour(), 0)); }
In init(), we connect the QSocket's connected(), connectionClosed(), readyRead(), and error(int) signals, and the two timers' timeout() signals, to our own slots. We also fill the Date and Approximate Time fields with default values based on the current date and time.
void TripPlanner::advanceProgressBar() { progressBar->setProgress(progressBar->progress() + 2); }
The advanceProgressBar() slot is connected to the progressBarTimer's timeout() signal. We advance the progress bar by two units. In Qt Designer, the progress bar's totalSteps property was set to 0, a special value meaning that the bar should behave as a busy indicator.
void TripPlanner::connectToServer() { listView->clear(); socket.connectToHost("tripserver.zugbahn.de", 6178); searchButton->setEnabled(false); stopButton->setEnabled(true); statusLabel->setText(tr("Connecting to server...")); connectionTimer.start(30 * 1000, true); progressBarTimer.start(200, false); blockSize = 0; }
The connectToServer() slot is executed when the user clicks Search to start a search. We call connectToHost() on the QSocket object to connect to the server, which we assume is accessible at port 6178 on the fictitious host tripserver. zugbahn.de. (If you want to try the example on your own machine, replace the host name with localhost.) The connectToHost() call is asynchronous; it always returns immediately. The connection is typically established later. The QSocket object emits the connected() signal when the connection is up and running, or error(int) (with an error code) if the connection failed.
Next, we update the user interface and start the two timers. The first timer, connectionTimer, is a single-shot timer that gets triggered when the connection has been idle for 30 seconds. The second timer, progressBarTimer, times out every 200 milliseconds to update the application's progress bar, giving a visual cue to the user that the application is working.
Finally, we set the blockSize variable to 0. The blockSize variable stores the length of the next block received from the server. We have chosen to use the value of 0 to mean that we don't yet know the size of the next block.
void TripPlanner::sendRequest() { QByteArray block; QDataStream out(block, IO_WriteOnly); out.setVersion(5); out << (Q_UINT16)0 << (Q_UINT8) 'S' << fromComboBox->currentText() << toComboBox->currentText() << dateEdit->date() << timeEdit->time(); if (departureRadioButton->isOn()) out << (Q_UINT8) 'D'; else out << (Q_UINT8) 'A'; out.device()->at(0); out << (Q_UINT16) (block.size() - sizeof(Q_UINT16)); socket.writeBlock(block.data(), block.size()); statusLabel->setText(tr("Sending request...")); }
The sendRequest() slot is executed when the QSocket object emits the connected() signal, indicating that a connection has been established. The slot's task is to generate a request to the server, with all the information entered by the user.
The request is a binary block with the following format:
Q_UINT16 |
Block size in bytes (excluding this field) |
Q_UINT8 |
Request type (always 'S') |
QString |
Departure city |
QString |
Arrival city |
QDate |
Date of travel |
QTime |
Approximate time of travel |
Q_UINT8 |
Time is for departure ('D') or arrival ('A') |
We first write the data to a QByteArray called block. We can't write the data directly to the QSocket because we don't know the size of the block, which must be sent first, until after we have put all the data into the block.
We initially write 0 as the block size, followed by the rest of the data. Then we call at(0) on the I/O device (a QBuffer created by QDataStream behind the scenes) to move back to the beginning of the byte array, and overwrite the initial 0 with the size of the block's data. The size is calculated by taking the block's size and subtracting sizeof(Q_UINT16) (that is, 2) to exclude the size field from the byte count. After that, we call writeBlock() on the QSocket to send the block to the server.
void TripPlanner::updateListView() { connectionTimer.start(30 * 1000, true); QDataStream in(&socket); in.setVersion(5); for (;;) { if (blockSize == 0) { if (socket.bytesAvailable() < sizeof(Q_UINT16)) break; in >> blockSize; } if (blockSize == 0xFFFF) { closeConnection(); statusLabel->setText(tr("Found %1 trip(s)") .arg(listView->childCount())); break; } if (socket.bytesAvailable() < blockSize) break; QDate date; QTime departureTime; QTime arrivalTime; Q_UINT16 duration; Q_UINT8 changes; QString trainType; in >> date >> departureTime >> duration >> changes >> trainType; arrivalTime = departureTime.addSecs(duration * 60); new QListViewItem(listView, date.toString(LocalDate), departureTime.toString(tr("hh:mm")), arrivalTime.toString(tr("hh:mm")), tr("%1 hr %2 min").arg(duration / 60) .arg(duration % 60), QString::number(changes), trainType); blockSize = 0; } }
The updateListView() slot is connected to the QSocket's readyRead() signal, which is emitted whenever the QSocket has received new data from the server. The first thing we do is to restart the single-shot connection timer. Whenever we receive some data from the server, we know that the connection is alive, so we set the timer running for another 30 seconds.
The server sends us a list of possible train trips that match the user's criteria. Each matching trip is sent as a single block, and each block starts with a size. What complicates the code in the for loop is that we don't necessarily get one block of data from the server at a time. We might have received an entire block, or just part of a block, or one and a half blocks, or even all of the blocks at once.
Figure 13.2. The Trip Server's blocks
So how does the for loop work? If the blockSize variable is 0, this means that we have not read the size of the next block. We try to read it (assuming there are at least 2 bytes available for reading). The server uses a size value of 0xFFFF to signify that there is no more data to receive, so if we read this value, we know that we have reached the end.
If the block size is not 0xFFFF, we try to read in the next block. First, we check to see if there are block size bytes available to read. If there are not, we stop there for now. The readyRead() signal will be emitted again when more data is available, and we will try again then.
Once we are sure that an entire block has arrived, we can safely use the >> operator on the QDataStream we set up on the QSocket to extract the information related to a trip, and we create a QListViewItem with that information. A block received from the server has the following format:
Q_UINT16 |
Block size in bytes (excluding this field) |
QDate |
Departure date |
QTime |
Departure time |
Q_UINT16 |
Duration (in minutes) |
Q_UINT8 |
Number of changes |
QString |
Train type |
At the end, we reset the blockSize variable to 0 to indicate that the next block's size is unknown and needs to be read.
void TripPlanner::closeConnection() { socket.close(); searchButton->setEnabled(true); stopButton->setEnabled(false); connectionTimer.stop(); progressBarTimer.stop(); progressBar->setProgress(0); }
The closeConnection() private function closes the connection to the TCP server, updates the user interface, and stops the timers. It is called from updateListView() when the 0xFFFF is read and from several other slots that we will cover shortly.
void TripPlanner::stopSearch() { statusLabel->setText(tr("Search stopped")); closeConnection(); }
The stopSearch() slot is connected to the Stop button's clicked() signal. Essentially it just calls closeConnection().
void TripPlanner::connectionTimeout() { statusLabel->setText(tr("Error: Connection timed out")); closeConnection(); }
The connectionTimeout() slot is connected to the connectionTimer's timeout() signal.
void TripPlanner::connectionClosedByServer() { if (blockSize != 0xFFFF) statusLabel->setText(tr("Error: Connection closed by " "server")); closeConnection(); }
The connectionClosedByServer() slot is connected to socket's connectionClosed() signal. If the server closes the connection and we have not yet received the 0xFFFF end-of-stream marker, we tell the user that an error occurred. We call closeConnection() as usual to update the user interface and to stop the timers.
void TripPlanner::error(int code) { QString message; switch (code) { case QSocket::ErrConnectionRefused: message = tr("Error: Connection refused"); break; case QSocket::ErrHostNotFound: message = tr("Error: Server not found"); break; case QSocket::ErrSocketRead: default: message = tr("Error: Data transfer failed"); } statusLabel->setText(message); closeConnection(); }
The error(int) slot is connected to socket's error(int) signal. We produce an error message based on the error code.
The main() function for the Trip Planner application looks just as we would expect:
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripPlanner tripPlanner; app.setMainWidget(&tripPlanner); tripPlanner.show(); return app.exec(); }
Now let's implement the server. The server consists of two classes: TripServer and ClientSocket. The TripServer class inherits QServerSocket, a class that allows us to accept incoming TCP connections. ClientSocket reimplements QSocket and handles a single connection. At any one time, there are as many ClientSocket objects in memory as there are clients being served.
class TripServer : public QServerSocket { public: TripServer(QObject *parent = 0, const char *name = 0); void newConnection(int socket); };
The TripServer class reimplements the newConnection() function from QServerSocket. This function is called whenever a client attempts to connect to the port the server is listening to.
TripServer::TripServer(QObject *parent, const char *name) : QServerSocket(6178, 1, parent, name) { }
In the TripServer constructor, we pass the port number (6178) to the base class constructor. The second argument, 1, is the number of pending connections we want to allow.
void TripServer::newConnection(int socketId) { ClientSocket *socket = new ClientSocket(this); socket->setSocket(socketId); }
In newConnection(), we create a ClientSocket object as a child of the TripServer object, and we set its socket ID to the number provided to us.
class ClientSocket : public QSocket { Q_OBJECT public: ClientSocket(QObject *parent = 0, const char *name = 0); private slots: void readClient(); private: void generateRandomTrip(const QString &from, const QString &to, const QDate &date, const QTime &time); Q_UINT16 blockSize; };
The ClientSocket class inherits from QSocket and encapsulates the state of a single client.
ClientSocket::ClientSocket(QObject *parent, const char *name) : QSocket(parent, name) { connect(this, SIGNAL(readyRead()), this, SLOT(readClient())); connect(this, SIGNAL(connectionClosed()), this, SLOT(deleteLater())); connect(this, SIGNAL(delayedCloseFinished()), this, SLOT(deleteLater())); blockSize = 0; }
In the constructor, we establish the necessary signalslot connections, and we set the blockSize variable to 0, indicating that we do not yet know the size of the block sent by the client.
The connectionClosed() and delayedCloseFinished() signals are connected to deleteLater(), a QObject-inherited function that deletes the object when control returns to Qt's event loop. This ensures that the ClientSocket object is deleted when the connection is closed by the peer or when a delayed close is finished. We will see what that means in a moment.
void ClientSocket::readClient() { QDataStream in(this); in.setVersion(5); if (blockSize == 0) { if (bytesAvailable() < sizeof(Q_UINT16)) return; in >> blockSize; } if (bytesAvailable() < blockSize) return; Q_UINT8 requestType; QString from; QString to; QDate date; QTime time; Q_UINT8 flag; in >> requestType; if (requestType == 'S') { in >> from >> to >> date >> time >> flag; srand(time.hour() * 60 + time.minute()); int numTrips = rand() % 8; for (int i = 0; i < numTrips; ++i) generateRandomTrip(from, to, date, time); QDataStream out(this); out << (Q_UINT16)0xFFFF; } close(); if (state() == Idle) deleteLater(); }
The readClient() slot is connected to QSocket's readyRead() signal. If blockSize is 0, we start by reading the blockSize; otherwise, we have already read it, and instead we check to see if a whole block has arrived. Once an entire block is ready for reading, we read it. We use the QDataStream directly on the QSocket (the this object) and read the fields using the >> operator.
Once we have read the client's request, we are ready to generate a reply. If this were a real application, we would look up the information in a train schedule database and try to find matching train trips. But here we will be content with a function called generateRandomTrip() that will generate a random trip. We call the function a random number of times, and we send 0xFFFF to signify the end of the data.
Finally, we close the connection. If the socket's output buffer is empty, the connection is terminated immediately and we call deleteLater() to delete this object when control returns to Qt's event loop. (This is safer than delete this.) Otherwise, QSocket will complete sending out all the data, and will then close the connection and emit the delayedCloseFinished() signal.
void ClientSocket::generateRandomTrip(const QString &, const QString &, const QDate &date, const QTime &time) { QByteArray block; QDataStream out(block, IO_WriteOnly); out.setVersion(5); Q_UINT16 duration = rand() % 200; out << (Q_UINT16)0 << date << time << duration << (Q_UINT8) 1 << QString("InterCity"); out.device()->at(0); out << (Q_UINT16) (block.size() - sizeof(Q_UINT16)); writeBlock(block.data(), block.size()); }
The generateRandomTrip() function shows how to send a block of data over a TCP connection. This is very similar to what we did in the client in the sendRequest() function (p.294). Once again, we write the block to a QByteArray so that we can determine its size before we send it using writeBlock().
int main(int argc, char *argv[]) { QApplication app(argc, argv); TripServer server; if (!server.ok()) { qWarning("Failed to bind to port"); return 1; } QPushButton quitButton(QObject::tr("&Quit"), 0); quitButton.setCaption(QObject::tr("Trip Server")); app.setMainWidget(&quitButton); QObject::connect(&quitButton, SIGNAL(clicked()), &app, SLOT(quit())); quitButton.show(); return app.exec(); }
In main(), we create a TripServer object and a QPushButton that enables the user to stop the server.
This completes our clientserver example. In this case, we used a block-oriented protocol that allows us to use QDataStream for reading and writing. If we wanted to use a line-oriented protocol, the simplest approach would be to use QSocket's canReadLine() and readLine() functions in a slot connected to the readyRead() signal:
QStringList lines; while (socket.canReadLine()) lines.append(socket.readLine());
We would then process each line that has been read. As for sending data, that can be done using a QTextStream on the QSocket.
The server implementation that we have used doesn't scale very well when there are lots of connections. The problem is that while we are processing a request, we don't handle the other connections. A more scalable approach would be to start a new thread for each connection. But QSocket can only be used in the thread that contains the event loop (the call to QApplication:: exec()), for reasons that are explained in Chapter 17 (Multithreading). The solution is to use the low-level QSocketDevice class directly, which doesn't rely on the event loop.