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