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

QT源码分析:QTcpServer的更多相关文章

  1. Qt源码分析之QObject

    原文:http://blog.csdn.net/oowgsoo/article/details/1529284 我感觉oowgsoo兄弟写的分析相当透彻,赞! 1.试验代码: #include < ...

  2. QT源码分析(从QApplication开始)

    QT源码分析 转载自:http://no001.blog.51cto.com/1142339/282130 今天,在给同学讲东西的时候,谈到了Qt源代码的问题,才发现自己对Qt机制的了解是在太少了,而 ...

  3. QT源码分析:QObject

    QT框架里面最大的特色就是在C++的基础上增加了元对象系统(Meta-Object System),而元对象系统里面最重要的内容就是信号与槽机制,这个机制是在C++语法的基础上实现的,使用了函数.函数 ...

  4. Qt源码分析之信号和槽机制

    Qt的信号和槽机制是Qt的一大特点,实际上这是和MFC中的消息映射机制相似的东西,要完成的事情也差不多,就是发送一个消息然后让其它窗口响应,当然,这里的消息是广义的说法,简单点说就是如何在一个类的一个 ...

  5. Qt源码分析之信号和槽机制(QMetaObject是一个内部struct)

    Qt的信号和槽机制是Qt的一大特点,实际上这是和MFC中的消息映射机制相似的东西,要完成的事情也差不多,就是发送一个消息然后让其它窗口响应,当然,这里的消息是广义的说法,简单点说就是如何在一个类的一个 ...

  6. Qt源码分析之QPointer

    QPointer是一个指针封装类,其作用类似于智能指针,但是它最大的特点应该是在指针的控制上,它希望一个Qt的指针(当然是从QObject派生的)可以同时被多个类拥有,这在界面编程中当然是很常见的事情 ...

  7. QT 源码分析--1

    Ref: http://blog.sina.com.cn/s/blog_6e80f1390100qoc0.html 安装qt之后(我使用的是online自动安装),安装目录下有\5.10.1\Src\ ...

  8. Qt之使用setWindowFlags方法遇到的问题(追踪进入QWidget的源码分析原因,最后用WINAPI解决问题)good

    一.简述 前段时间在使用setWindowFlags方法时遇到了一个坑,具体情况是想通过窗口界面上一个checkBox来控制窗口当前状态是否置顶,而Qt提供了Qt::WindowStaysOnTopH ...

  9. Qt update刷新之源码分析(一)

    在做GUI开发时,要让控件刷新,会调用update函数:那么在调用了update函数后,Qt究竟基于什么原理.执行了什么代码使得屏幕上有变化?本文就带大家来探究探究其内部源码. Qt手册中关于QWid ...

随机推荐

  1. git上传者姓名修改

    只需要两个指令 git config user.name 和 git config –global user.name 在控制台中输入git config user.name获取当前的操作名称 修改名 ...

  2. 关于vue的v-for遍历不显示问题

    实属不才,因为好久没看vue导致忘光了,然后发生了这么小的一个问题,惭愧. 注:vue的注册的el一定要放嘴最外层,不要和v-for放在一起,否则不会显示,因为可以这样讲,el包含的是一个容器,而v- ...

  3. python写入excel(方式1)

    import xlsxwriter li=["张三","李四","王五","周六","王琪",&qu ...

  4. JAVA项目部署到云服务器

    转自:(此处更详细)http://blog.csdn.net/gulu_gulu_jp/article/details/50994003 一.前言 前面我们已经尝过了在云服务器上部署代码的甜头了,现在 ...

  5. 手机代理调试Charles Proxy和Fiddler

    一.Charles Proxy Charles是一个HTTP代理/HTTP监控/反向代理的工具. 使用它开发者可以查看设备的HTTP和SSL/HTTPS网络请求.返回.HTTP头信息 (cookies ...

  6. BZOJ 4197: [Noi2015]寿司晚宴 状压dp+质因数分解

    挺神的一道题 ~ 由于两个人选的数字不能有互质的情况,所以说对于一个质因子来说,如果 1 选了,则 2 不能选任何整除该质因子的数. 然后,我们发现对于 1 ~ 500 的数字来说,只可能有一个大于 ...

  7. Noip2019暑期训练1

    题目名称 时空定位 棋子移动 高精度乘法 数独游戏 存盘文件名 location piece mul sudoku 输入文件名 location.in piece.in mul.in sudoku.i ...

  8. 前端base64加密

    一.Base64编码表 码值 字符 码值 字符 码值 字符 码值 字符 0 A 16 Q 32 g 48 w 1 B 17 R 33 h 49 x 2 C 18 S 34 i 50 y 3 D 19 ...

  9. Java实现RS485串口通信,发送和接收数据进行解析

    最近项目有一个空气检测仪,需要得到空气检测仪的实时数据,保存到数据库当中.根据了解得到,硬件是通过rs485进行串口通讯的,需要发送16进制命令给仪器,然后通过轮询来得到数据. 需要先要下载RXTX的 ...

  10. clion ctrl+鼠标左键不能调到函数实现

    问题:ctrl+鼠标左键只能在函数定义和申明之间跳转,不能跳转到implement,如果按ctrl+shift+B会报“no implement”. 解决:更改CMakeList文件,更改版本号. c ...