码迷,mamicode.com
首页 > 其他好文 > 详细

QT源码分析:QTcpServer

时间:2018-09-24 14:49:59      阅读:174      评论:0      收藏:0      [点我收藏+]

标签:应用程序   end   pos   就会   string   mit   ISE   can   ++   

    最近在看有关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复用,具体要看你使用的版本。

QT源码分析:QTcpServer

标签:应用程序   end   pos   就会   string   mit   ISE   can   ++   

原文地址:https://www.cnblogs.com/WushiShengFei/p/9681885.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!