Qt網路程式設計QTcpServer和QTcpSocket的理解
前一段時間通過除錯Qt原始碼,大致瞭解了Qt的事件機制、訊號槽機制。畢竟能力和時間有限。有些地方理解的並不是很清楚。
開發環境:linux((fedora 17),Qt版本(qt-everywhere-opensource-src-4.7.3)。
Qt網路程式設計比較常用的兩個類:QTcpServer和QTcpSocket。當然還有UDP的類(在這就不介紹了)。
這兩個類的操作比較簡單。
QTcpServer的基本操作:
1、呼叫listen監聽埠。
2、連線訊號newConnection,在槽函式裡呼叫nextPendingConnection獲取連線進來的socket。
QTcpSocket的基本能操作:
1、呼叫connectToHost連線伺服器。
2、呼叫waitForConnected判斷是否連線成功。
3、連線訊號readyRead槽函式,非同步讀取資料。
4、呼叫waitForReadyRead,阻塞讀取資料。
在main函式中呼叫 app.exec()之後會進入事件迴圈。在Qt的事件迴圈中回撥用QEventLoop::processEvents。在這個函式中又會呼叫QAbstractEventDispatcher::processEvents。
Qt為不同的平臺,提供了不同的EventDispatcher。本人用的是linux。接下來會呼叫QEventDispatcherUNIX::processEvents。doSelect,select。
通過除錯原始碼可以證實,函式呼叫的順序。其實Qt網路程式設計,底層是通過select實現的。可能跟跨平臺有關係吧,採用select。linux有比select更好的API(poll,epoll)。
通過除錯,可以發現newConnection和readyRead這兩個訊號都和select有關係。
因為呼叫的是select所以伺服器端肯定會有監視檔案描述符數量問題。select預設的是1024。Qt預設的也是1024。伺服器端在呼叫nextPendingConnection。會建立一個QTcpSocket物件。這個物件在QTcpServer物件刪除的時候會自動刪除。也可以手動刪除,避免浪費記憶體,可以在該物件斷開的時候,刪除該物件。該物件在斷開的時候,select呼叫會清除該描述符,不會影響檔案描述符的數量。如果記憶體足夠用的話,不需要關心該物件。
接下來介紹readyRead訊號。該訊號用的比較多。
該訊號當有資料要讀的時候,會觸發該訊號。不過在觸發該訊號之前,Qt會嘗試讀取bytesToRead資料,存在內部緩衝區中。
下面是Qt原始碼片段
qint64 bytesToRead = socketEngine->bytesAvailable();
#ifdef Q_OS_LINUX
if (bytesToRead > 0) // ### See setSocketDescriptor()
bytesToRead += addToBytesAvailable;
#endif
if (bytesToRead == 0) {
// Under heavy load, certain conditions can trigger read notifications
// for socket notifiers on which there is no activity. If we continue
// to read 0 bytes from the socket, we will trigger behavior similar
// to that which signals a remote close. When we hit this condition,
// we try to read 4k of data from the socket, which will give us either
// an EAGAIN/EWOULDBLOCK if the connection is alive (i.e., the remote
// host has _not_ disappeared).
bytesToRead = 4096;
}
if (readBufferMaxSize && bytesToRead > (readBufferMaxSize - readBuffer.size()))
bytesToRead = readBufferMaxSize - readBuffer.size();
// Read from the socket, store data in the read buffer.
char *ptr = readBuffer.reserve(bytesToRead);
qint64 readBytes = socketEngine->read(ptr, bytesToRead);
if (readBytes == -2) {
// No bytes currently available for reading.
readBuffer.chop(bytesToRead);
return true;
}
readBuffer.chop(int(bytesToRead - (readBytes < 0 ? qint64(0) : readBytes)));
qint64 QNativeSocketEnginePrivate::nativeBytesAvailable() const
{
int nbytes = 0;
// gives shorter than true amounts on Unix domain sockets.
qint64 available = 0;
#ifdef Q_OS_SYMBIAN
if (::ioctl(socketDescriptor, FIONREAD, (char *) &nbytes) >= 0)
#else
if (qt_safe_ioctl(socketDescriptor, FIONREAD, (char *) &nbytes) >= 0)
#endif
available = (qint64) nbytes;
return available;
}
讀取資料的步驟:
1、通過ioctl獲取系統能夠讀取的資料長度。
2、在linux系統中多加4K資料(跟linux域套接字有關係)。
3、如果呼叫setReadBufferSize()設定緩衝區大小的話。重新計算bytesToRead。從程式碼中可以看出來,只有bytesToRead大的話,才重新計算。如果readBufferMaxSize設定過大,不會重新計算。
4、呼叫read函式嘗試讀取bytesToRead長度資料。返回實際讀取的資料長度readBytes。通過除錯可以看出readBytes和bytesToRead相差並不是很多。
bytesAvailable()函式返回緩衝區長度。如果沒設定readBufferMaxSize,該函式返回值主要取決於ioctl系統呼叫。該系統呼叫跟系統當前執行的狀態有關係。
可能會出現的問題:
1、當觸發readyRead訊號,但是緩衝區的長度小於另一端傳送的資料。這樣就會觸發多次readyReady訊號。如果一次槽函式裡面讀取緩衝區的長度,資料就會接受不全,進行資料處理肯定會出問題。Qt的例子中提供了一種方法。在傳送資料的頭部加上資料的長度。只有當bytesAvailable大於資料的長度時,才讀取資料。
2、系統API裡面呼叫read是從系統緩衝區裡面讀取資料。如果系統緩衝區滿的話。以前的就會被覆蓋。但是Qt裡面也存在緩衝區。如果一端傳送資料。另一端並不從Qt緩衝區讀取資料。那麼Qt就會無限制的從系統緩衝區中讀出資料放置自己內部緩衝區。最後肯定會出現堆疊滿的情況,系統異常退出。