• 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复用,具体要看你使用的版本。

  • 相关阅读:
    css优化总结
    几种常用的图片格式
    css布局总结
    第四章复习题
    4.9,4.10
    4.8
    4.7指针
    libffi
    代理模式
    Redis 汇总
  • 原文地址:https://www.cnblogs.com/WushiShengFei/p/9681885.html
Copyright © 2020-2023  润新知