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