by Rainer Schmid & Mark Summerfield |
GUI applications that are network clients shouldn't freeze up during networking operations. This article compares two different ways of keeping a networking client's user interface responsive: using the non-blocking QSocket class, and using a blocking QSocketDevice in a separate thread.
One approach to networking is to use non-blocking networking operations within the GUI thread. This approach can be tricky because we must maintain a state machine so that we know what to do whenever we are notified of a networking event, but because the operations are non-blocking, it still leaves the user interface responsive.
An alternative is to push all the networking into a separate thread of execution. This approach allows us to use blocking networking operations and still ensures that the user interface remains responsive because the thread scheduler is responsible for giving each thread a fair share of resource time. The threading approach simplifies the logic because we can perform each operation in sequence; if an operation is slow, it will simply block until it's completed, but without blocking the rest of the application.
We will show both approaches. We will take as our example an application that must periodically download a set of files from a custom server. The client will first ask the server which files have been updated, and will then download each of these files. We will encapsulate the functionality in a class called UpdateFileLists and provide this class with a startUpdate() slot that we assume the client application will call when an update is required.
Using an Non-Blocking QSocket |
The QSocket class is higher-level than QSocketDevice and this simplifies some of the networking operations. But because it is non-blocking (to keep the user interface responsive), we must keep track of the state of the operations. Let's start with the definition of the class that uses QSocket:
class UpdateFileLists : public QObject { Q_OBJECT public: UpdateFileLists(); public slots: void startUpdate(); private slots: void connected(); void readyRead(); void error() { state = Idle; files.clear(); numLines = 0; socket.close(); } private: void getNextFile(); enum State { Idle, GetFileList, GetFiles }; QSocket socket; State state; QStringList files; int numLines; QFile file; };
In this example and the one that follows, the UpdateFileLists class is used in the same way. An instance of the class is created (for example, as a member of the application's MainWindow class), and startUpdate() is called either periodically (using a QTimer) or through an action.
The QSocket class is non-blocking and emits signals when its status changes (for example, to inform us that a connection has been established or that there is new data to read). We must connect to these signals and do a certain amount of book-keeping to keep track of what we're doing and where we're up to.
UpdateFileLists::UpdateFileLists() { state = Idle; connect(&socket, SIGNAL(connected()), SLOT(connected())); connect(&socket, SIGNAL(readyRead()), SLOT(readyRead())); connect(&socket, SIGNAL(error(int)), SLOT(error())); }
In the constructor, we set up the relevant signal--slot connections, and begin in the Idle state.
void UpdateFileLists::startUpdate() { state = Idle; files.clear(); numLines = 0; socket.close(); socket.connectToHost("localhost", 4949); }
In an application, the startUpdate() slot would be connected from an action, or from a timer, to initiate the download. It clears the list of files, sets the number of lines read so far to 0, closes the socket (in case it's open from a previous download), and initiates a new connection. As a result, either the connected() or the error() slot will be called next.
void UpdateFileLists::connected() { QTextStream out(&socket); out << "UPDATE\r\n"; state = GetFileList; }
Once the connection is established, we send an UPDATE command to our server and change to the GetFileList state. If no error occurs, the next slot to be invoked will be readyRead(); this function is the heart of the UpdateFileLists class.
void UpdateFileLists::readyRead() { QTextStream in(&socket); QString line; while (socket.canReadLine()) { if (state == GetFileList) { if (numLines == 0) { numLines = in.readLine().toInt(); } else { files.append(in.readLine()); --numLines; if (numLines == 0) { state = GetFiles; getNextFile(); } } } else if (state == GetFiles) { if (numLines == 0) { numLines = in.readLine().toInt(); } else { QTextStream fileStream(&file); fileStream << socket.readLine(); --numLines; if (numLines == 0) { file.close(); files.pop_front(); if (files.isEmpty()) { state = Idle; socket.close(); } else { getNextFile(); } } } } else { break; } } }
The server's response to the UPDATE command is to send a line containing a count of the number of file names that need updating, followed by one file name per line for count lines. Once the list of files has been read, the state is changed to GetFiles and a call to getNextFile() is made. We'll come back to the server's response in GetFiles mode shortly.
void UpdateFileLists::getNextFile() { file.setName(files[0]); if (!file.open(IO_WriteOnly)) return; QTextStream out(&socket); out << "GET " << files[0] << "\r\n"; }
This function sets the target file name to the first one in the list of files. (This name is popped off the list once the file has been read.) We then send a GET request to our server.
The server's response to a GET request is to send a line containing a count of the number of lines in the file, followed by the lines of the file. The readyRead() slot reads all the server's responses and data, which is why we need to keep track of which state it's in. Once the count of lines has been read in GetFiles mode, we open a text stream and read in the lines of the file and write them to disk. Once the file has been read, getNextFile() is called if there are more files to read; otherwise, the state is set back to Idle and the socket closed.
The main advantage of using QSocket like this is that we can work at quite a high level. For example, we can use QStrings when reading and writing data, which we cannot do if we use QSocketDevice directly. But the disadvantage is that we must do quite a bit of book-keeping, tracking the current file, the current line count, the list of files, and the state we're in.
Threading a Synchronous QSocketDevice |
By executing our networking operations in a separate thread of execution, we can make them synchronous (blocking) since the thread scheduler will make sure that the GUI thread gets resource time, even when the networking thread is blocked.
In Qt 3, the QSocket class cannot be used within a non-GUI thread so we must use the lower-level QSocketDevice class instead. Here's the definition of the UpdateFileLists class:
class UpdateFileLists : public QObject { Q_OBJECT public: UpdateFileLists() {} public slots: void startUpdate() { thread.start(); } private: MyThread thread; };
In this example, the real work is passed on to a QThread subclass:
class MyThread : public QThread { public: void run(); private: void writeToSocket(const QString& str); QSocketDevice *socket; };
All the processing can be done synchronously, since we can rely on the thread scheduler to ensure that the user interface gets a fair share of processor time. This means that we can do all the processing in the run() function. The writeToSocket() function is a helper that writes a QString to a QSocketDevice.
void MyThread::run() { const int MaxLen = 256; char line[MaxLen]; socket = new QSocketDevice; socket->setBlocking(true); if (!socket->connect(QHostAddress(0x7f000001), 4949)) return; writeToSocket("UPDATE\r\n"); socket->readLine(line, MaxLen); int numLines = QString(line).toInt(); QStringList files; while (numLines) { socket->readLine(line, MaxLen); QString lineStr = QString::fromUtf8(line).stripWhiteSpace(); files.append(lineStr); --numLines; } for (int i = 0; i < (int)files.count(); ++i) { QFile file(files[i]); if (!file.open(IO_WriteOnly)) continue; writeToSocket(QString("GET %1\r\n").arg(files[i])); socket->readLine(line, MaxLen); int numLines = QString(line).toInt(); QTextStream fileStream(&file); while (numLines) { socket->readLine(line, MaxLen); fileStream << line; --numLines; } file.close(); } socket->close(); delete socket; }
The code is shorter and easier than the previous example because we don't have to keep track of state. We can simply issue commands and respond to them sequentially. On the other hand, because QSocketDevice is lower level than QSocket it deals with bytes rather than QStrings. For this reason, we had to create a char line[MaxLen] buffer and have made the assumption that no line will exceed 256 characters. The stripWhiteSpace() call is necessary to avoid every line being the size of the buffer.
We must use QHostAddress() rather than localhost in the socket's connect() call because QSocketDevice requires an IP address. Compare this with the higher-level QSocket, which will look up the IP address of a host name itself.
We create a socket device and set it to be blocking. It doesn't matter if the socket blocks waiting to send or receive data since we are using it in a non-GUI thread so the GUI will not be slowed down. We send an UPDATE command to our server (which is a request for a list of file names that need uploading), but because we're using a blocking connection we can read the result immediately (i.e., as soon as it's ready). We pick up the number of files and then each file name, one file name per line. Once we have the list of files, we iterate over them one by one. For each file, we send GET to the server (which tells the server to send the specified file), and then read how many lines the file has and then read these lines and write them to the corresponding local file.
void MyThread::writeToSocket(const QString &str) { QByteArray ba; QTextOStream out(ba); out << str; int wrote = socket->writeBlock(ba.data(), ba.size()); while (wrote < (int)ba.size()) wrote += socket->writeBlock(ba.data() + wrote, ba.size() - wrote); }
The writeToSocket() function converts its string parameter into bytes and then writes them to the socket. When we call writeBlock(), it is possible that not all of the data is written. For this reason, we have the while loop which executes until all the data has been sent. This kind of loop only works properly for blocking sockets; on a non-blocking socket, it is simply a busy loop. A helper function like this wasn't necessary for QSocket.
Copyright © 2004 Trolltech | Trademarks |