演示如何為網絡服務創建客戶端。
QTcpSocket 支持 2 種一般網絡編程方式:
waitFor...()
函數 (如
QTcpSocket::waitForConnected
()) 以掛起調用綫程,直到操作完成,而不是連接到信號。
實現非常類似於 Fortune 客戶端 範例,而不是擁有 QTcpSocket 作為 main 類成員,以主綫程進行異步組網,以單獨綫程執行所有網絡操作和使用 QTcpSocket 的阻塞 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.
將從 FortuneThread 類開始,處理網絡代碼。
class FortuneThread : public QThread { Q_OBJECT public: FortuneThread(QObject *parent = 0); ~FortuneThread(); void requestNewFortune(const QString &hostName, quint16 port); void run() override; signals: void newFortune(const QString &fortune); void error(int socketError, const QString &message); private: QString hostName; quint16 port; QMutex mutex; QWaitCondition cond; bool quit; };
FortuneThread 是 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.
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { QMutexLocker locker(&mutex); this->hostName = hostName; this->port = port; if (!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 QWaitCondition::wakeOne () call later.
void FortuneThread::run() { mutex.lock(); QString serverName = hostName; quint16 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
可重入
but
not
綫程安全
, 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.
run() 函數現在進入循環:
while (!quit) {
const int Timeout = 5 * 1000;
QTcpSocket socket;
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 以發射 connected() or error() .
if (!socket.waitForConnected(Timeout)) {
emit error(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
QTcpSocket::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.
QTcpSocket::waitForConnected
(),像其它
waitFor...()
函數,屬於
QTcpSocket
's
阻塞 API
.
在此語句後,我們有要操控的被連接套接字。
QDataStream in(&socket);
in.setVersion(QDataStream::Qt_4_0);
QString fortune;
現在,可以創建 QDataStream 對象,把套接字傳遞給 QDataStream 's constructor, and as in the other client examples we set the stream protocol version to QDataStream::Qt_4_0 .
do {
if (!socket.waitForReadyRead(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}
in.startTransaction();
in >> fortune;
} while (!in.commitTransaction());
We proceed by initiating a loop that waits for the fortune string data by calling QTcpSocket::waitForReadyRead (). If it returns false, we abort the operation. After this statement, we start a stream read transaction. We exit the loop when QDataStream::commitTransaction () returns true, which means successful fortune string loading. The resulting fortune is delivered by emitting newFortune():
mutex.lock();
emit newFortune(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 QWaitCondition::wait (). At this point, we can go back to requestNewFortune() and look closed at the call to wakeOne():
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { ... if (!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.
現在對於 BlockingClient 類:
class BlockingClient : public QWidget { Q_OBJECT public: BlockingClient(QWidget *parent = 0); private slots: void requestNewFortune(); void showFortune(const QString &fortune); void displayError(int socketError, const QString &message); void enableGetFortuneButton(); private: QLabel *hostLabel; QLabel *portLabel; QLineEdit *hostLineEdit; QLineEdit *portLineEdit; QLabel *statusLabel; QPushButton *getFortuneButton; QPushButton *quitButton; QDialogButtonBox *buttonBox; FortuneThread thread; QString currentFortune; };
BlockingClient 非常類似於 Client 類在 Fortune 客戶端 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, SIGNAL(newFortune(QString)),
this, SLOT(showFortune(QString)));
connect(&thread, SIGNAL(error(int,QString)),
this, SLOT(displayError(int,QString)));
We connect our FortuneThread's two signals newFortune() and error() (which are somewhat similar to QTcpSocket::readyRead () 和 QTcpSocket::error () in the previous example) to requestNewFortune() and displayError().
void BlockingClient::requestNewFortune() { 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:
void BlockingClient::showFortune(const QString &nextFortune) { if (nextFortune == currentFortune) { requestNewFortune(); return; } currentFortune = nextFortune; statusLabel->setText(currentFortune); getFortuneButton->setEnabled(true); }
這裏,簡單把收到的 Fortune 顯示成自變量。
文件:
另請參閱 Fortune 客戶端範例 and Fortune 服務器範例 .