Blocking Fortune Client Example¶
Demonstrates how to create a client for a network service.
QTcpSocket
supports two general approaches to network programming:
The asynchronous (non-blocking) approach. Operations are scheduled and performed when control returns to Qt’s event loop. When the operation is finished,
QTcpSocket
emits a signal. For example,connectToHost()
returns immediately, and when the connection has been established,QTcpSocket
emitsconnected()
.The synchronous (blocking) approach. In non-GUI and multithreaded applications, you can call the
waitFor...()
functions (e.g.,waitForConnected()
) to suspend the calling thread until the operation has completed, instead of connecting to signals.
The implementation is very similar to the Fortune Client example, but instead of having QTcpSocket
as a member of the main class, doing asynchronous networking in the main thread, we will do all network operations in a separate thread and use QTcpSocket
‘s blocking API.
The purpose of this example is to demonstrate a pattern that you can use to simplify your networking code, without losing responsiveness in your user interface. Use of Qt’s blocking network API often leads to simpler code, but because of its blocking behavior, it should only be used in non-GUI threads to prevent the user interface from freezing. But contrary to what many think, using threads with QThread
does not necessarily add unmanagable complexity to your application.
We will start with the FortuneThread class, which handles the network code.
class FortuneThread(QThread): Q_OBJECT # public FortuneThread(QObject parent = None) ~FortuneThread() def requestNewFortune(hostName, port): def run(): signals: def newFortune(fortune): def error(socketError, message): # private hostName = QString() port = quint16() mutex = QMutex() cond = QWaitCondition() quit = bool()
FortuneThread is a QThread
subclass that provides an API for scheduling requests for fortunes, and it has signals for delivering fortunes and reporting errors. You can call requestNewFortune() to request a new fortune, and the result is delivered by the newFortune() signal. If any error occurs, the error() signal is emitted.
It’s important to notice that requestNewFortune() is called from the main, GUI thread, but the host name and port values it stores will be accessed from FortuneThread’s thread. Because we will be reading and writing FortuneThread’s data members from different threads concurrently, we use QMutex
to synchronize access.
def requestNewFortune(self, hostName, port): locker = QMutexLocker(mutex) self.hostName = hostName self.port = port if (not isRunning()) start() else: cond.wakeOne()
The requestNewFortune() function stores the host name and port of the fortune server as member data, and we lock the mutex with QMutexLocker
to protect this data. We then start the thread, unless it is already running. We will come back to the wakeOne()
call later.
def run(self): mutex.lock() serverName = hostName serverPort = port mutex.unlock()
In the run() function, we start by acquiring the mutex lock, fetching the host name and port from the member data, and then releasing the lock again. The case that we are protecting ourselves against is that requestNewFortune()
could be called at the same time as we are fetching this data. QString
is reentrant but not thread-safe, and we must also avoid the unlikely risk of reading the host name from one request, and port of another. And as you might have guessed, FortuneThread can only handle one request at a time.
The run() function now enters a loop:
while (not quit) { Timeout = 5 * 1000 socket = QTcpSocket() socket.connectToHost(serverName, serverPort)
The loop will continue requesting fortunes for as long as quit is false. We start our first request by creating a QTcpSocket
on the stack, and then we call connectToHost()
. This starts an asynchronous operation which, after control returns to Qt’s event loop, will cause QTcpSocket
to emit connected()
or error()
.
if (not socket.waitForConnected(Timeout)) { error.emit(socket.error(), socket.errorString()) return
But since we are running in a non-GUI thread, we do not have to worry about blocking the user interface. So instead of entering an event loop, we simply call waitForConnected()
. This function will wait, blocking the calling thread, until QTcpSocket
emits connected() or an error occurs. If connected() is emitted, the function returns true; if the connection failed or timed out (which in this example happens after 5 seconds), false is returned. waitForConnected()
, like the other waitFor...()
functions, is part of QTcpSocket
‘s blocking API.
After this statement, we have a connected socket to work with.
in = QDataStream(socket) in.setVersion(QDataStream.Qt_4_0) fortune = QString()
Now we can create a QDataStream
object, passing the socket to QDataStream
‘s constructor, and as in the other client examples we set the stream protocol version to Qt_4_0
.
do { if (not socket.waitForReadyRead(Timeout)) { error.emit(socket.error(), socket.errorString()) return in.startTransaction() in >> fortune } while (not in.commitTransaction())
We proceed by initiating a loop that waits for the fortune string data by calling waitForReadyRead()
. If it returns false, we abort the operation. After this statement, we start a stream read transaction. We exit the loop when commitTransaction()
returns true, which means successful fortune string loading. The resulting fortune is delivered by emitting newFortune():
mutex.lock() newFortune.emit(fortune) cond.wait(mutex) serverName = hostName serverPort = port mutex.unlock()
The final part of our loop is that we acquire the mutex so that we can safely read from our member data. We then let the thread go to sleep by calling wait()
. At this point, we can go back to requestNewFortune() and look closed at the call to wakeOne():
def requestNewFortune(self, hostName, port): ... if (not isRunning()) start() else: cond.wakeOne()
What happened here was that because the thread falls asleep waiting for a new request, we needed to wake it up again when a new request arrives. QWaitCondition
is often used in threads to signal a wakeup call like this.
FortuneThread::~FortuneThread() mutex.lock() quit = True cond.wakeOne() mutex.unlock() wait()
Finishing off the FortuneThread walkthrough, this is the destructor that sets quit to true, wakes up the thread and waits for the thread to exit before returning. This lets the while
loop in run() will finish its current iteration. When run() returns, the thread will terminate and be destroyed.
Now for the BlockingClient class:
class BlockingClient(QWidget): Q_OBJECT # public BlockingClient(QWidget parent = None) slots: = private() def requestNewFortune(): def showFortune(fortune): def displayError(socketError, message): def enableGetFortuneButton(): # private hostLabel = QLabel() portLabel = QLabel() hostLineEdit = QLineEdit() portLineEdit = QLineEdit() statusLabel = QLabel() getFortuneButton = QPushButton() quitButton = QPushButton() buttonBox = QDialogButtonBox() thread = FortuneThread() currentFortune = QString()
BlockingClient is very similar to the Client class in the Fortune Client example, but in this class we store a FortuneThread member instead of a pointer to a QTcpSocket
. When the user clicks the “Get Fortune” button, the same slot is called, but its implementation is slightly different:
connect(thread, FortuneThread::newFortune, self, BlockingClient::showFortune) connect(thread, FortuneThread::error, self, BlockingClient::displayError)
We connect our FortuneThread’s two signals newFortune() and error() (which are somewhat similar to readyRead()
and error()
in the previous example) to requestNewFortune() and displayError().
def requestNewFortune(self): getFortuneButton.setEnabled(False) thread.requestNewFortune(hostLineEdit.text(), portLineEdit.text().toInt())
The requestNewFortune() slot calls FortuneThread::requestNewFortune(), which shedules the request. When the thread has received a new fortune and emits newFortune(), our showFortune() slot is called:
def showFortune(self, nextFortune): if (nextFortune == currentFortune) { requestNewFortune() return currentFortune = nextFortune statusLabel.setText(currentFortune) getFortuneButton.setEnabled(True)
Here, we simply display the fortune we received as the argument.
© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.