最近在看有關IO復用方面的內容,自己也用標准c++庫實現了select模型、iocp模型、poll模型。回過頭來很想了解QT的socket是基於什么模型來實現的,所以看了QT關於TcpServer實現的相關源碼,現在將所了解的內容記錄下來,希望對感興趣的朋友有所幫助。
1.我們先從QTcpServer的構造函數來看,下面是QTcpServer的構造函數原型:
QTcpServer::QTcpServer(QObject *parent) : QObject(*new QTcpServerPrivate, parent) { Q_D(QTcpServer); #if defined(QTCPSERVER_DEBUG) qDebug("QTcpServer::QTcpServer(%p)", parent); #endif d->socketType = QAbstractSocket::TcpSocket; }
我們可以看到首先創建了一個QTcpServerPrivate的參數類,在QT源碼中,每一個類都有一個參數類,參數類的類名是:類名+Private,這個類主要放置QTcpServer類中會使用到的一些成員對象,而QTcpServer里面只會定義方法不會有成員對象了。然后構造函數內部實現很簡單:
Q_D(QTcpServer);這個宏實際上就是取到QTcpServerPrivate對象的指針賦給變量d,
d->socketType = QAbstractSocket::TcpSocket;把套接字類型設置為Tcp。
那么第一步構造函數的工作就結束了。
2. 當我們調用listen函數以后,tcpserver就啟動了,之后連接,接收數據和發送數據完成都可以通過信號來接收,那么QT具體是如何實現等待連接和等待接收數據的呢,對於不同平台又是怎么實現的,我們來分析一下listen函數做了什么工作。
(1)首先判斷是否已是監聽狀態,是的話就直接返回。
Q_D(QTcpServer); if (d->state == QAbstractSocket::ListeningState) { qWarning("QTcpServer::listen() called when already listening"); return false; }
(2)設置協議類型,IP地址端口號等。
QAbstractSocket::NetworkLayerProtocol proto = address.protocol(); QHostAddress addr = address; #ifdef QT_NO_NETWORKPROXY static const QNetworkProxy &proxy = *(QNetworkProxy *)0; #else QNetworkProxy proxy = d->resolveProxy(addr, port); #endif delete d->socketEngine;
(3)創建socketEngine對象,socketEngine的類型是QAbstractSocketEngine,QAbstractSocketEngine定義了很多與原始套接字機制相似的函數如bind、listen、accept等方法,也實現了:waitForRead、writeDatagram、read等函數。所以可以看到我們調用QSocket的讀寫方法其實都是由QAbstractSocketEngine類來實現的。但是QAbstractSocketEngine本身是一個抽象類,是不能被實例化的,listen函數里面調用了QAbstractSocketEngine類的靜態函數createSocketEngine來創建對象。
d->socketEngine = QAbstractSocketEngine::createSocketEngine(d->socketType, proxy, this); if (!d->socketEngine) { d->serverSocketError = QAbstractSocket::UnsupportedSocketOperationError; d->serverSocketErrorString = tr("Operation on socket is not supported"); return false; }
我們在來看一下createSocketEngine具體是怎么實現的:
QAbstractSocketEngine *QAbstractSocketEngine::createSocketEngine(QAbstractSocket::SocketType socketType, const QNetworkProxy &proxy, QObject *parent) { return new QNativeSocketEngine(parent); }
這個不是完整代碼,但是前面的所有條件判斷完后,最終就是調用這一句返回一個QNativeSocketEngine對象,QNativeSocketEngine繼承了QAbstractSocketEngine 類,實現了QAbstractSocketEngine 的所有功能,在這個類的具體代碼中我們可以看到一些做平台判斷的代碼,也可以找到與平台相關的套接字函數,我們可以看到QNativeSocketEngine的實現不只一個文件,有qnativesocketengine_unix.cpp、qnativesocketengine_win.cpp、qnativesocketengine_winrt.cpp。所以當你在windows平台編譯程序的時候編譯器包含的是qnativesocketengine_win.cpp文件,在linux下編譯的時候包含的是qnativesocketengine_unix.cpp文件,所以QT通過一個抽象類和不同平台的子類來實現跨平台的套接字機制。
(4)回到TcpServer的listen函數,創建socketEngine對象以后,開始調用bind,listen等函數完成最終的socket設置。
#ifndef QT_NO_BEARERMANAGEMENT //copy network session down to the socket engine (if it has been set) d->socketEngine->setProperty("_q_networksession", property("_q_networksession")); #endif if (!d->socketEngine->initialize(d->socketType, proto)) { d->serverSocketError = d->socketEngine->error(); d->serverSocketErrorString = d->socketEngine->errorString(); return false; } proto = d->socketEngine->protocol(); if (addr.protocol() == QAbstractSocket::AnyIPProtocol && proto == QAbstractSocket::IPv4Protocol) addr = QHostAddress::AnyIPv4; d->configureCreatedSocket(); if (!d->socketEngine->bind(addr, port)) { d->serverSocketError = d->socketEngine->error(); d->serverSocketErrorString = d->socketEngine->errorString(); return false; } if (!d->socketEngine->listen()) { d->serverSocketError = d->socketEngine->error(); d->serverSocketErrorString = d->socketEngine->errorString(); return false; }
(5)設置信號接收
d->socketEngine->setReceiver(d); d->socketEngine->setReadNotificationEnabled(true);
setReceiver傳入TcpServerPrivate對象,從函數名可以看出是設置一個接收信息的對象,所以當套接字有新信息時,就會回調TcpServerPrivate對象的相關函數來實現消息通知。設置完消息接收對象以后,調用setReadNotificationEnabled(true)來啟動消息監聽。這個函數的實現如下:
void QNativeSocketEngine::setReadNotificationEnabled(bool enable) { Q_D(QNativeSocketEngine); if (d->readNotifier) { d->readNotifier->setEnabled(enable); } else if (enable && d->threadData->hasEventDispatcher()) { d->readNotifier = new QReadNotifier(d->socketDescriptor, this); d->readNotifier->setEnabled(true); } }
我們看到這個函數是創建了一個QReadNotifier對象,而QReadNotifier的定義如下:
class QReadNotifier : public QSocketNotifier { public: QReadNotifier(qintptr fd, QNativeSocketEngine *parent) : QSocketNotifier(fd, QSocketNotifier::Read, parent) { engine = parent; } protected: bool event(QEvent *) override; QNativeSocketEngine *engine; }; bool QReadNotifier::event(QEvent *e) { if (e->type() == QEvent::SockAct) { engine->readNotification(); return true; } else if (e->type() == QEvent::SockClose) { engine->closeNotification(); return true; } return QSocketNotifier::event(e); }
我們可以看到QReadNotifier繼承了QSocketNotifier,而QSocketNotifier是一個消息處理類,主要用來監聽文件描述符活動的,也就是當文件描述符狀態變更時則會觸發相應信息,它可以監聽三種狀態:Read、Write、Exception。而我們這里用到的QReadNotifier它監聽的是Read事件,也就是當套接字句柄有可讀消息(連接信息也是可讀信息的一種)時就會回調event函數,而在event里面回調了engine->readNotification();readNotification函數的實現如下:
void QAbstractSocketEngine::readNotification() { if (QAbstractSocketEngineReceiver *receiver = d_func()->receiver) receiver->readNotification(); }
engine的readNotification又回調了receiver的readNotification函數,還記得我們上面說的嗎,receiver實際上就是QTcpServerPrivate,所以到這里,QT實現了當有新的客戶端連接時,通知QTcpServerPrivate對象的功能,所以我們看一下QTcpServerPrivated的readNotification實現:
void QTcpServerPrivate::readNotification() { Q_Q(QTcpServer); for (;;) { if (pendingConnections.count() >= maxConnections) { #if defined (QTCPSERVER_DEBUG) qDebug("QTcpServerPrivate::_q_processIncomingConnection() too many connections"); #endif if (socketEngine->isReadNotificationEnabled()) socketEngine->setReadNotificationEnabled(false); return; } int descriptor = socketEngine->accept(); if (descriptor == -1) { if (socketEngine->error() != QAbstractSocket::TemporaryError) { q->pauseAccepting(); serverSocketError = socketEngine->error(); serverSocketErrorString = socketEngine->errorString(); emit q->acceptError(serverSocketError); } break; } #if defined (QTCPSERVER_DEBUG) qDebug("QTcpServerPrivate::_q_processIncomingConnection() accepted socket %i", descriptor); #endif q->incomingConnection(descriptor); QPointer<QTcpServer> that = q; emit q->newConnection(); if (!that || !q->isListening()) return; } }
我們可以看到這個函數里面調用了socketEngine->accept();獲取套接字句柄,然后傳給q->incomingConnection(descriptor);創建QTcoSocket對象,最后發送emit q->newConnection();信號,這個信號有用過QTcpServer的應該就很熟悉了吧,所以QT通過內部消息機制實現了套接字的異步通信,而對外提供的函數即支持同步機制也支持異步機制,調用者可以選擇通過信號槽機制來實現異步,也可以調用如:waitforread,waitforconnect等函數來實現同步等待,實際上waitforread等同步函數是通過函數內部的循環來檢查消息標志,當標志為可讀或者函數超時時則返回。
3.QSocketNotifier的實現
我們在上面說了通過QSocketNotifier,我們可以實現當套接字有可讀或可寫信號時調用event函數來實現異步通知。但是QSocketNotifier又是如何知道socket什么時候發生變化的呢。QSocketNotifier的實現和QT的消息處理機制是息息相關的,要完全講清楚就必須講到QT的消息機制,這個已經超出對QTcpServer的討論了,當然我們還是可以把其中比較關鍵的代碼抽取出來分析一下。首先不同平台的消息處理機制都是不一樣的,所以QSocketNotifier在不同平台下的實現也是不一樣的,我們首先來看一下windows平台下是如何實現的。
(1)注冊SocketNotifier
QSocketNotifier::QSocketNotifier(qintptr socket, Type type, QObject *parent) : QObject(*new QSocketNotifierPrivate, parent) { Q_D(QSocketNotifier); d->sockfd = socket; d->sntype = type; d->snenabled = true; if (socket < 0) qWarning("QSocketNotifier: Invalid socket specified"); else if (!d->threadData->eventDispatcher.load()) qWarning("QSocketNotifier: Can only be used with threads started with QThread"); else d->threadData->eventDispatcher.load()->registerSocketNotifier(this); }
我們看到QSocketNotifier的構造函數里面需要傳入socket句柄以及要監聽的類型,read,write或者error。然后調用了QSocketNotifierPrivate的registerSocketNotifier函數把自己注冊進去,這使得當有消息觸發的時候可以調用這個對象的event函數。
(2)調用WSAAsyncSelect
在registerSocketNotifier函數里面會調用WSAAsyncSelect函數,這個函數的原型是:int PASCAL FAR WSAAsyncSelect (SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent);
s 要監聽的套接字句柄
hWnd 標識一個在網絡事件發生時需要接收消息的窗口句柄.
wMsg 在網絡事件發生時要接收的消息.
lEvent位屏蔽碼,用於指明應用程序感興趣的網絡事件集合.
這個函數的作用是告訴操作系統當套接字發送改變時,發送一條消息給我們的應用程序,發送的消息內容就是我們傳入的wMsg,QT在調用的時候傳入了一個消息類型WM_QT_SOCKETNOTIFIER,所以當我們的應用程序接收到系統返回的WM_QT_SOCKETNOTIFIER類型的消息我們就知道是有某個套接字狀態改變了。
(3)qt_internal_proc
qt_internal_proc是消息回調函數,當系統發送消息給程序后,會進入這個處理函數,在其中有一段代碼用於處理WM_QT_SOCKETNOTIFIER消息的代碼:
if (message == WM_QT_SOCKETNOTIFIER) { // socket notifier message int type = -1; switch (WSAGETSELECTEVENT(lp)) { case FD_READ: case FD_ACCEPT: type = 0; break; case FD_WRITE: case FD_CONNECT: type = 1; break; case FD_OOB: type = 2; break; case FD_CLOSE: type = 3; break; } if (type >= 0) { Q_ASSERT(d != 0); QSNDict *sn_vec[4] = { &d->sn_read, &d->sn_write, &d->sn_except, &d->sn_read }; QSNDict *dict = sn_vec[type]; QSockNot *sn = dict ? dict->value(wp) : 0; if (sn == nullptr) { d->postActivateSocketNotifiers(); } else { Q_ASSERT(d->active_fd.contains(sn->fd)); QSockFd &sd = d->active_fd[sn->fd]; if (sd.selected) { Q_ASSERT(sd.mask == 0); d->doWsaAsyncSelect(sn->fd, 0); sd.selected = false; } d->postActivateSocketNotifiers(); const long eventCode = WSAGETSELECTEVENT(lp); if ((sd.mask & eventCode) != eventCode) { sd.mask |= eventCode; QEvent event(type < 3 ? QEvent::SockAct : QEvent::SockClose); QCoreApplication::sendEvent(sn->obj, &event); } } } return 0; }
這段代碼的功能主要是檢查事件類型,然后查詢是哪個句柄的事件,通過句柄與事件類型可以關聯到我們注冊的對象,然后調用QCoreApplication::sendEvent給我們的對象發送事件,在這個函數里最終就是調用到QSocketNotifier的event函數。至此整個套接字從應用層到QT底層到系統API的整個流程就很清楚了。所以我們可以看到QT是通過WSAAsyncSelect來實現IO復用的,相比於select模型,這種模型是異步的,而且沒有監聽數量的上限。
講完了windows平台的,我們在來看一下linux平台下的實現,第一步和windows的一樣都是在QSocketNotifier構造函數里面注冊對象本身用於接收事件。
(1)registerSocketNotifier
在這個函數里面主要是將對象和套接字句柄作為映射放入socketNotifiers里面。
QHash<int, QSocketNotifierSetUNIX> socketNotifiers;
(2)processEvents
這個函數是用於處理所有消息的,在這其中一段用於處理套接字相關
switch (qt_safe_poll(d->pollfds.data(), d->pollfds.size(), tm)) { case -1: perror("qt_safe_poll"); break; case 0: break; default: nevents += d->threadPipe.check(d->pollfds.takeLast()); if (include_notifiers) nevents += d->activateSocketNotifiers(); break; }
(3)qt_safe_poll
qt_safe_poll調用了qt_ppoll,而qt_ppoll里面是如此定義的:
static inline int qt_ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts) { #if QT_CONFIG(poll_ppoll) || QT_CONFIG(poll_pollts) return ::ppoll(fds, nfds, timeout_ts, nullptr); #elif QT_CONFIG(poll_poll) return ::poll(fds, nfds, timespecToMillisecs(timeout_ts)); #else return qt_poll(fds, nfds, timeout_ts); #endif }
這里可以通過QT_CONFIG的標志判斷來采取其中一種實現,qt_poll是QT自己實現的函數,實際上采用的是select模式,在早期的版本中應該是用的select模式,QT5.7以后的版本采用了poll模式,我所用的版本是QT5.9用的就是poll模式,之所以使用poll取代select是因為select模式監聽的套接字長度是用的定長的數組,所以在運行期是無法擴展的,只要套接字超過FD_SETSIZE就會返回錯誤,在Linux默認的設置中FD_SETSIZE為1024。
(4)activateSocketNotifiers
在processEvents函數中調用了qt_safe_poll來檢查是否有套接字事件,如果有事件需要處理則調用activateSocketNotifiers函數,而這個函數中調用了QCoreApplication::sendEvent(notifier, &event);將消息回饋給QSocketNotifier。到此linux下的socket完整流程我們也知道了,在linux下可能采用select或者poll來實現io復用,具體要看你使用的版本。