This example uses QTcpSocket, and is intended to be run alongside the Fortune Server example or the Threaded Fortune Server example.
說明如何未一個網絡服務創建一個client,這個例子使用了QTcpSocket,需要與Fortune Server或Threaded Fortune Server一同運行。
This example uses a simple QDataStream-based data transfer protocol to request a line of text from a fortune server (from the Fortune Server example). The client requests a fortune by simply connecting to the server. The server then responds with a 16-bit
(quint16) integer containing the length of the fortune text, followed by a QString.
這個例子使用了一個簡單的基於QDataStream數據傳輸協議來從fortune server請求一行text(從Fortune Server例子)。客戶端通過簡單的連接服務器來請求一個fortune。服務器就反饋一個16bit的整數,隨後一個包含該長度的fortune text(是QString類型)。
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, QTcpSocketemits a signal. For example, QTcpSocket::connectToHost() returns immediately, and when the connection has been established, QTcpSocket emits connected().
- The synchronous (blocking) approach. In non-GUI and multithreaded applications, you can call the waitFor...() functions (e.g., QTcpSocket::waitForConnected()) to suspend the calling thread until the operation has completed, instead of connecting to signals.
異步方法:控制返回qt的事件loop後安排和執行operations,當operations結束後,qtcpsocket發射一個信號。例如,qtcpsocket::connecttohost()一返回,連接建立,qtcpsocket發射連接的信號。
同步方法:在非gui和多進程應用中,可以調用waitfor...()函數,如qtcpsocket::waitforconnected()來懸掛調用進程直到完成operations,而不是發射信號。
class Client : public QDialog
{
Q_OBJECT
public:
Client(QWidget *parent = 0);
private slots:
void requestNewFortune();
void readFortune();
void displayError(QAbstractSocket::SocketError socketError);
void enableGetFortuneButton();
void sessionOpened();
private:
QLabel *hostLabel; // server name 標籤
QLabel *portLabel; // server port 標籤
QComboBox *hostCombo; // host下拉列表
QLineEdit *portLineEdit; // port編輯框
QLabel *statusLabel; // 狀態顯示欄
QPushButton *getFortuneButton; // get fortune按鈕
QPushButton *quitButton; // 退出按鈕
QDialogButtonBox *buttonBox; // 用於封裝get fortune和quit按鈕
QTcpSocket *tcpSocket; // 套接字
QString currentFortune; // 字符串用於顯示於狀態顯示欄
quint16 blockSize; // 塊大小
QNetworkSession *networkSession; // 網絡session
};
Other than the widgets that make up the GUI, the data members include a QTcpSocket pointer, a copy of the fortune text currently displayed, and the size of the packet we are currently reading (more on this later).
The socket is initialized in the Client constructor. We'll pass the main widget as parent, so that we won't have to worry about deleting the socket:
不是widgets建立gui,數據成員,包括qtcpsocket指針,顯示fortune text的一個副本,及將閱讀包的大小。socket在client的構造函數中初始化,作爲parent傳送給main widget,因此而不用檢測socket。
Client::Client(QWidget *parent)
: QDialog(parent), networkSession(0)
{
...
tcpSocket = new QTcpSocket(this); // 構造函數裏面初始化了tcpsocket對象
The only QTcpSocket signals we need in this example are QTcpSocket::readyRead(), signifying that data has been received, and QTcpSocket::error(), which we will use to catch any connection errors:唯一需要的qtcpsocket信號是qtcpsocket::readyread(),標示數據已經接受,而qtcpsocket::error()用來捕捉連接錯誤。
...
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readFortune())); // 綁定信號,接受完畢後讀取fortune()
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(requestNewFortune())); // 綁定錯誤,請求新的fortune
Clicking the Get Fortune button will invoke the requestNewFortune() slot:
點擊get fortune按鈕會喚起requestNewFortune()槽:
void Client::requestNewFortune()
{
getFortuneButton->setEnabled(false); // 屏蔽後續請求
blockSize = 0; // 初始化包大小爲0
tcpSocket->abort(); // 結束tcpsocket
tcpSocket->connectToHost(hostCombo->currentText(),
portLineEdit->text().toInt()); // 通過目標信息連接到host中
}
In this slot, we initialize blockSize to 0, preparing to read a new block of data. Because we allow the user to click Get Fortune before the previous connection finished closing, we start off by aborting the previous connection by calling
QTcpSocket::abort(). (On an unconnected socket, this function does nothing.) We then proceed to connecting to the fortune server by calling QTcpSocket::connectToHost(), passing the hostname and port from the user interface as arguments.在這個slot中,我們初始化blocksize爲0,準備讀取一個新的塊數據,由於允許用戶在前一個連接完成關閉之前點擊get fortune,因此,用absort()終止前一個連接爲開端。(在一個未連接的socket,這個函數不做任何事情)然後,就着手用connectToHost()連接fortune服務器,從界面參數傳遞hostname和端口參數。
As a result of calling connectToHost(), one of two things can happen:
The connection is established. In this case, the server will send us a fortune. QTcpSocket will emit readyRead() every time it receives a block of data.
An error occurs. We need to inform the user if the connection failed or was broken. In this case, QTcpSocket will emit error(), and Client::displayError() will be called.
Let's go through the error() case first:
調用了connectToHost()函數,意味着兩件事情的發生。
連接建立,這種情況下,服務器發送給我們一個fortune,QTcpSocket每接受到一個塊數據就發射一次readyRead()
發生錯誤,需要通知用戶連接失敗,這種情況下,QTcpSocket發射error(),同時Client::displayError()會被調用。
void Client::displayError(QAbstractSocket::SocketError socketError)
{
switch (socketError) {
case QAbstractSocket::RemoteHostClosedError: // 遠程服務器關閉錯誤
break;
case QAbstractSocket::HostNotFoundError: // 服務器未找到錯誤
QMessageBox::information(this, tr("Fortune Client"),
tr("The host was not found. Please check the "
"host name and port settings."));
break;
case QAbstractSocket::ConnectionRefusedError: // 連接拒絕錯誤
QMessageBox::information(this, tr("Fortune Client"),
tr("The connection was refused by the peer. "
"Make sure the fortune server is running, "
"and check that the host name and port "
"settings are correct."));
break;
default:
QMessageBox::information(this, tr("Fortune Client"),
tr("The following error occurred: %1.")
.arg(tcpSocket->errorString()));
}
getFortuneButton->setEnabled(true);
}
We pop up all errors in a dialog using QMessageBox::information(). QTcpSocket::RemoteHostClosedError is silently ignored, because the fortune server protocol ends with the server closing the connection.
Now for the readyRead() alternative. This signal is connected to Client::readFortune():
用QMessageBox::information()輸出所有錯誤,QTcpSocket::RemoteHostClosedError 一定程度會忽略,由於fortune服務器協議與遠程服務器關閉一同停止。
然後readyRead()是可選的,信號連接連接到了Client::readFortune();
void Client::readFortune()
{
QDataStream in(tcpSocket); // 將套接字的數據讀入QDataStream中
in.setVersion(QDataStream::Qt_4_0); // 設置讀入QDataStream的版本::Qt_4_0
if (blockSize == 0) { // 讀入一個quint16都不行,就可以直接返回了
if (tcpSocket->bytesAvailable() < (int)sizeof(quint16))
return;
in >> blockSize; // 從in數據流讀入到blockSize中
}
if (tcpSocket->bytesAvailable() < blockSize) // 目標讀取的長度大於可供讀寫長度
return;
The protocol is based on QDataStream, so we start by creating a stream object, passing the socket to QDataStream's constructor. We then explicitly set the protocol version of the stream to QDataStream::Qt_4_0 to ensure that we're using the
same version as the fortune server, no matter which version of Qt the client and server use.Now, TCP is based on sending a stream of data, so we cannot expect to get the entire fortune in one go. Especially on a slow network, the data can be received in several small fragments. QTcpSocket buffers up all incoming data and emits readyRead() for every new block that arrives, and it is our job to ensure that we have received all the data we need before we start parsing. The server's response starts with the size of the packet, so first we need to ensure that we can read the size, then we will wait until QTcpSocket has received the full packet.
這個協議是給予QDataStream,因此開始就創建了一個stream對象,傳遞socket到QDataStream的構造函數中,然後明確設置stream協議的版本以保證使用的與server相同的協議,無論哪個Qt協議都可以的。
現在,TCP基於發送一個流數據,因此不能期待一次性得到完整的fortune,尤其是緩慢網絡的時候,數據會在數個時間片收到,QTcpSocket緩衝區緩衝所有傳來的數據然後在每個新的塊數據到達時候發射readRead(),保證在執行解析前收到所需的所有數據。服務器的響應以包大小爲開頭,因此,我們需要確保可以讀取到packetsize,然後等待直到QTcpSocket接收到整個包。
QString nextFortune;
in >> nextFortune;
if (nextFortune == currentFortune) {
QTimer::singleShot(0, this, SLOT(requestNewFortune()));
return;
}
currentFortune = nextFortune;
statusLabel->setText(currentFortune);
getFortuneButton->setEnabled(true);
}
We proceed by using QDataStream's streaming operator to read the fortune from the socket into a QString. Once read, we can call QLabel::setText() to display the fortune.用QDataStream的流運算來從socket讀取fortune到一個QString,每讀取一次,就調用QLabel::setText()來顯示fortune。