QT源碼分析:QTcpServer


    最近在看有關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的類型是QAbstractSocketEngineQAbstractSocketEngine定義了很多與原始套接字機制相似的函數如bindlistenaccept等方法,也實現了:waitForReadwriteDatagramread等函數。所以可以看到我們調用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.cppqnativesocketengine_win.cppqnativesocketengine_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是一個消息處理類,主要用來監聽文件描述符活動的,也就是當文件描述符狀態變更時則會觸發相應信息,它可以監聽三種狀態:ReadWriteException。而我們這里用到的QReadNotifier它監聽的是Read事件,也就是當套接字句柄有可讀消息(連接信息也是可讀信息的一種)時就會回調event函數,而在event里面回調了engine->readNotification();readNotification函數的實現如下:

void QAbstractSocketEngine::readNotification()
{
    if (QAbstractSocketEngineReceiver *receiver = d_func()->receiver)
        receiver->readNotification();
}

 

    enginereadNotification又回調了receiverreadNotification函數,還記得我們上面說的嗎,receiver實際上就是QTcpServerPrivate,所以到這里,QT實現了當有新的客戶端連接時,通知QTcpServerPrivate對象的功能,所以我們看一下QTcpServerPrivatedreadNotification實現:

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。然后調用了QSocketNotifierPrivateregisterSocketNotifier函數把自己注冊進去,這使得當有消息觸發的時候可以調用這個對象的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給我們的對象發送事件,在這個函數里最終就是調用到QSocketNotifierevent函數。至此整個套接字從應用層到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_pollQT自己實現的函數,實際上采用的是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復用,具體要看你使用的版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM