一种基于Qt的可伸缩的全异步C/S架构server实现(二) 网络传输
二、网络传输模块
模块相应代码命名空间 (namespace ZPNetwork)
模块相应代码存储目录 (\ZoomPipeline_FuncSvr\network)
2.1 模块结构
网络传输模块负责管理监听器,并依据各个传输线程眼下的负荷,把新申请接入的客户套接字描写叙述符引导到最空暇的传输线程中运行“接受连接(Accept)”操作。该模块由例如以下几个类组成。
1、zp_net_Engine类,派生自Qobject。模块的外部接口类。同一时候也是功能管理者。提供了设置监听器、配置线程池的功能。
2、zp_netListenThread类:派生自QObject。用于绑定在各个监听线程的事件循环中,不断的接受client连接请求。该类会在信号中把套接字描写叙述符(socketdescriptor)泵出,由zp_net_Engine类进行负荷均衡,选取当前负荷最小的传输线程(zp_netTransThread)接受该接入申请。
3、zp_netTransThread类:派生自QObject。用于绑定在各个传输线程的事件循环中,详细承担传输数据。一个zp_netTransThread线程能够承担多个client的收发请求。
4、ZP_TcpServer类:派生自QtcpServer。
重载了ZP_TcpServer::incomingConnection,不在监听线程中进行Accept操作,而是直接发出evt_NewClientArrived信号,把套接字描写叙述符(socketdescriptor)泵出。由zp_net_Engine类进行负荷均衡,选取当前负荷最小的传输线程(zp_netTransThread)接受该接入申请。
这四个类的合作关系图例如以下
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29sZGVuaGF3a2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
2.2 系统原理
为了提供基于线程池的TCP服务。zp_net_engine类有几个重要成员。以下,依照一次client发起连接的过程,逆向的逐一来介绍这些类的合作原理.
2.2.1 监听器与监听线程
1、监听器ZP_TcpServer
系统执行时。负责监听工作的是 QtcpServer 派生类,名称叫ZP_TcpServer。该类重载了 QtcpServer的incomingConnection()方法1。当网络中一个client发起连接时,这个函数会被立马调用。在本派生类中。并没有直接产生套接字。它只触发了一个称为“evt_NewClientArrived”的信号2。这个信号把套接字描写叙述符泵出给接受者,用于在其它的线程中创建套接字所用。其流程见2.2.2节所述。
2、监听器线程对象zp_netListenThread
ZP_TcpServer类的实例详细是由zp_netListenThread类中一个指针 m_tcpServer操作的。
m_tcpServer是一个指向ZP_TcpServe类实例的指针(參见zp_netlistenthread.h )。
该实例在zp_netListenThread::startListen()中创建。StartListen是一个关键的函数。创建了ZP_TcpServer对象。核心代码例如以下:
m_tcpServer = new ZP_TcpServer(this);
connect (m_tcpServer,&ZP_TcpServer::evt_NewClientArrived,this,&zp_netListenThread::evt_NewClientArrived,Qt::QueuedConnection);
上面两行代码中,第一行创建一个监听服务。第二行。把监听服务的evt_NewClientArrived事件直接和zp_netListenThread 的 同名事件连接起来。
3、操作监听器的模块接口类zp_net_Engine
zp_netListenThread类本身是从Qobject派生。它本身不是一个线程对象,而是被“绑定”在一个线程对象中执行的。
一个进程能够拥有若干监听port,这些监听port相应了不同的zp_netListenThread对象。这些监听线程对象由zp_net_Engine类管理,存储在这个类的成员变量中。以下两个成员变量
//This map stores listenThreadObjects
QMap<QString,zp_netListenThread *> m_map_netListenThreads;
//Internal Threads to hold each listenThreadObjects' message Queue
QMap<QString,QThread *> m_map_netInternalListenThreads;
第一个存储了各个port的线程对象,第二个存储了各个port的线程。
因为详细下达监听任务的线程是主线程(UI),但运行任务的线程是工作线程,所以,全部的指令均不是通过直接的函数调用来实现。取而代之的是使用Qt的信号与槽。比方。UIbutton被点击。则触发了startListen 信号。转而由zp_netListenThread的startListen槽来响应。
这里须要注意的是,因为Qt的信号与槽系统是一种广播系统。意味着一个zp_net_Engine类管理多个zp_netListenThread对象时。zp_net_Engine发出的信号会被全部zp_netListenThread对象接收。因此,信号与槽中含有一个唯一标示,用于指示本次信号触发是为了操作详细哪个对象。这样的技术在类似的场合被多次使用。
void zp_net_Engine::AddListeningAddress(QString id,const QHostAddress & address , quint16 nPort,bool bSSLConn /*= true*/)
{
if (m_map_netListenThreads.find(id)==m_map_netListenThreads.end())
{
//Start Thread
QThread * pThread = new QThread(this);
zp_netListenThread * pListenObj = new zp_netListenThread(id,address,nPort,bSSLConn);
pThread->start();
//m_mutex_listen.lock();
m_map_netInternalListenThreads[id] = pThread;
m_map_netListenThreads[id] = pListenObj;
//m_mutex_listen.unlock();
//Bind Object to New thread
connect(this,&zp_net_Engine::startListen,pListenObj,&zp_netListenThread::startListen,Qt::QueuedConnection);
connect(this,&zp_net_Engine::stopListen,pListenObj,&zp_netListenThread::stopListen,Qt::QueuedConnection);
connect(pListenObj,&zp_netListenThread::evt_Message,this,&zp_net_Engine::evt_Message,Qt::QueuedConnection);
connect(pListenObj,&zp_netListenThread::evt_ListenClosed,this,&zp_net_Engine::on_ListenClosed,Qt::QueuedConnection);
connect(pListenObj,&zp_netListenThread::evt_NewClientArrived,this,&zp_net_Engine::on_New_Arrived_Client,Qt::QueuedConnection); pListenObj->moveToThread(pThread);
//Start Listen Immediately
emit startListen(id);
}
else
emit evt_Message(this,"Warning>"+QString(tr("This ID has been used.")));
}
2.2.2 接受连接过程
client发起接入请求后,首先触发了ZP_TcpServer的incomingConnection方法。
在以下这种方法中,套接字的描写叙述符作为事件的參数被泵出。
void ZP_TcpServer::incomingConnection(qintptr socketDescriptor)
{
emit evt_NewClientArrived(socketDescriptor);
}
上面的信号相应的槽为zp_net_Engine::on_New_Arrived_Client槽函数。在这个函数中,网络模块首先从当前可用的传输线程中确定最空暇的那个线程,而后把套接字描写叙述符转交给传输线程。这个部分的核心代码:
void zp_net_Engine::on_New_Arrived_Client(qintptr socketDescriptor)
{
zp_netListenThread * pSource = qobject_cast<zp_netListenThread *>(sender());
if (!pSource)
{
emit evt_Message(this,"Warning>"+QString(tr("Non-zp_netListenThread type detected.")));
return;
} emit evt_Message(this,"Info>" + QString(tr("Incomming client arriverd.")));
int nsz = m_vec_NetTransThreads.size();
int nMinPay = 0x7fffffff;
int nMinIdx = -1; for (int i=0;i<nsz && nMinPay!=0;i++)
{
if (m_vec_NetTransThreads[i]->isActive()==false ||
m_vec_NetTransThreads[i]->SSLConnection()!=pSource->bSSLConn()
)
continue;
int nPat = m_vec_NetTransThreads[i]->CurrentClients(); if (nPat<nMinPay)
{
nMinPay = nPat;
nMinIdx = i;
}
//qDebug()<<i<<" "<<nPat<<" "<<nMinIdx;
}
//...
if (nMinIdx>=0 && nMinIdx<nsz)
emit evt_EstablishConnection(m_vec_NetTransThreads[nMinIdx],socketDescriptor);
else
{
emit evt_Message(this,"Warning>"+QString(tr("Need Trans Thread Object for clients.")));
}
}
上面的代码中, evt_EstablishConnection 事件携带了由均衡策略确定的承接线程、socketDescriptor 描写叙述符。
这个事件广播给全部的传输线程对象。在各个对象的incomingConnection槽中,详细生成用于传输的套接字对象.注意, 这个槽函数是执行在各个传输线程的事件循环中的,因此,创建的套接字直接属于特定线程.
/**
* @brief This slot dealing with multi-thread client socket accept.
* accepy works start from zp_netListenThread::m_tcpserver, end with this method.
* the socketDescriptor is delivered from zp_netListenThread(a Listening thread)
* to zp_net_Engine(Normally in main-gui thread), and then zp_netTransThread.
*
* @param threadid if threadid is not equal to this object, this message is just omitted.
* @param socketDescriptor socketDescriptor for incomming client.
*/
void zp_netTransThread::incomingConnection(QObject * threadid,qintptr socketDescriptor)
{
if (threadid!=this)
return;
QTcpSocket * sock_client = 0;
if (m_bSSLConnection)
sock_client = new QSslSocket(this);
else
sock_client = new QTcpSocket(this);
if (sock_client)
{
//Initial content
if (true ==sock_client->setSocketDescriptor(socketDescriptor))
{
connect(sock_client, &QTcpSocket::readyRead,this, &zp_netTransThread::new_data_recieved,Qt::QueuedConnection);
connect(sock_client, &QTcpSocket::disconnected,this,&zp_netTransThread::client_closed,Qt::QueuedConnection);
connect(sock_client, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(displayError(QAbstractSocket::SocketError)),Qt::QueuedConnection);
connect(sock_client, &QTcpSocket::bytesWritten, this, &zp_netTransThread::some_data_sended,Qt::QueuedConnection);
m_mutex_protect.lock();
m_clientList[sock_client] = 0;
m_mutex_protect.unlock();
if (m_bSSLConnection)
{
QSslSocket * psslsock = qobject_cast<QSslSocket *>(sock_client);
assert(psslsock!=NULL);
QString strCerPath = QCoreApplication::applicationDirPath() + "/svr_cert.pem";
QString strPkPath = QCoreApplication::applicationDirPath() + "/svr_privkey.pem";
psslsock->setLocalCertificate(strCerPath);
psslsock->setPrivateKey(strPkPath);
connect(psslsock, &QSslSocket::encrypted,this, &zp_netTransThread::on_encrypted,Qt::QueuedConnection);
psslsock->startServerEncryption();
}
emit evt_NewClientConnected(sock_client);
emit evt_Message(sock_client,"Info>" + QString(tr("Client Accepted.")));
}
else
sock_client->deleteLater();
} }
2.2.3 数据接收
在成功创建了套接字后, 数据的收发都在传输线程中执行了.当套接字收到数据后,简单的触发事件
evt_Data_recieved
void zp_netTransThread::new_data_recieved()
{
QTcpSocket * pSock = qobject_cast<QTcpSocket*>(sender());
if (pSock)
{
QByteArray array = pSock->readAll();
int sz = array.size();
g_mutex_sta.lock();
g_bytesRecieved +=sz;
g_secRecieved += sz;
g_mutex_sta.unlock();
emit evt_Data_recieved(pSock,array);
}
}
2.2.4数据发送
虽然Qt的套接字本身具备缓存,塞入多大的数据都会成功, 可是本实现仍旧使用额外的队列, 每次缓存一个固定长度的片段并顺序发送. 这种优点,是能够给代码使用者一个机会,来增加代码检查缓冲区的大小,并作一些持久化的工作. 比方,队列超过100MB后,就把兴许的数据缓存在磁盘上, 而不是继续放在内存中,
实现这个策略的变量是两个缓存.
//sending buffer, hold byteArraies.
QMap<QObject *,QList<QByteArray> > m_buffer_sending; QMap<QObject *,QList<qint64> > m_buffer_sending_offset;
第一个缓存存储各个套接字的队列.还有一个存储各个数据块的发送偏移. 这样做是有性能缺陷的, 更好的办法是从 QTcpSocket 派生自己的类,并把各个套接字的缓存直接存储在派生类实例中去. 在本实现中, 直接使用了 QTcpSocket和QSSLSocket类, 因而有一定的性能损失.
一个槽方法 SendDataToClient 负责接受发送数据的请求.
void zp_netTransThread::SendDataToClient(QObject * objClient,QByteArray dtarray)
{
m_mutex_protect.lock();
if (m_clientList.find(objClient)==m_clientList.end())
{
m_mutex_protect.unlock();
return;
}
m_mutex_protect.unlock();
QTcpSocket * pSock = qobject_cast<QTcpSocket*>(objClient);
if (pSock&&dtarray.size())
{
QList<QByteArray> & list_sock_data = m_buffer_sending[pSock];
QList<qint64> & list_offset = m_buffer_sending_offset[pSock];
if (list_sock_data.empty()==true)
{
qint64 bytesWritten = pSock->write(dtarray.constData(),qMin(dtarray.size(),m_nPayLoad));
if (bytesWritten < dtarray.size())
{
list_sock_data.push_back(dtarray);
list_offset.push_back(bytesWritten);
}
}
else
{
list_sock_data.push_back(dtarray);
list_offset.push_back(0);
}
}
}
在上面的函数中,将检查队列是否为空.为空的话,将触发 QTcpSocket::write方法发出m_nPayload大小的数据块.当这些数据块发送完成,将触发QTcpSocket::bytesWritten事件,由以下的槽响应.
/**
* @brief this slot will be called when internal socket successfully
* sent some data. in this method, zp_netTransThread object will check
* the sending-queue, and send more data to buffer.
*
* @param wsended
*/
void zp_netTransThread::some_data_sended(qint64 wsended)
{
g_mutex_sta.lock();
g_bytesSent +=wsended;
g_secSent += wsended;
g_mutex_sta.unlock();
QTcpSocket * pSock = qobject_cast<QTcpSocket*>(sender());
if (pSock)
{
emit evt_Data_transferred(pSock,wsended);
QList<QByteArray> & list_sock_data = m_buffer_sending[pSock];
QList<qint64> & list_offset = m_buffer_sending_offset[pSock];
while (list_sock_data.empty()==false)
{
QByteArray & arraySending = *list_sock_data.begin();
qint64 & currentOffset = *list_offset.begin();
qint64 nTotalBytes = arraySending.size();
assert(nTotalBytes>=currentOffset);
qint64 nBytesWritten = pSock->write(arraySending.constData()+currentOffset,qMin((int)(nTotalBytes-currentOffset),m_nPayLoad));
currentOffset += nBytesWritten;
if (currentOffset>=nTotalBytes)
{
list_offset.pop_front();
list_sock_data.pop_front();
}
else
break;
}
}
}
2.2.5 其它工作
在传输终止后, 会进行一定的清理. 对于多线程的传输,最重要的是确保各个对象的生存期. 有兴趣的读者能够使用 sharedptr来管理动态分配的对象, 这样操作起来会非常方便. 在本范例中, 全部代码均进行了 7*24 调试.
下一章,将介绍流水线线程池的原理和实现.
一种基于Qt的可伸缩的全异步C/S架构server实现(二) 网络传输的更多相关文章
- 一种基于Qt的可伸缩的全异步C/S架构server实现(一) 综述
本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/Sclient-服务集群应用需求而搭建. 连接监听.传输数据.数据处理均在独立的线程池中进行,依据特定任务不同,可安排负责监听.传输.处理 ...
- 一种基于Qt的可伸缩的全异步C/S架构server实现(五) 单层无中心集群
五.单层无中心集群 对40万用户规模以内的server.使用星形的无中心连接是较为简便的实现方式.分布在各个物理server上的服务进程共同工作.每一个进程承担若干连接.为了实现这个功能,须要解决几个 ...
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(流浪小狗,六篇,附下载地址)
本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/S客户端-服务集群应用需求而搭建.连接监听.数据传输.数据处理均在独立的线程池中进行,根据特定任务不同,可安排负责监听.传输.处理的线程数 ...
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/S客户端-服务集群应用需求而搭建.连接监听.数据传输.数据处理均在独立的线程池中进行,根据特定任务不同,可安排负责监听.传输.处理的线程数 ...
- 一种基于C51单片机的非抢占式的操作系统架构
摘 要:从Keil C51的内存空间管理方式入手,着重讨论实时操作系统在任务调度时的重入问题,分析一些解决重入的基本方式与方法:分析实时操作系统任务调度的占先性,提出非占先的任务调度是能更适合于Kei ...
- 利用ZoomPipeline迅速实现基于线程池的全异步TCP点对点代理
在博文<一种基于Qt的可伸缩的全异步C/S架构服务器实现>中提到的高度模块化的类可以进行任意拆解,实现非常灵活的功能.今天,我们来看一看一个公司局域网访问英特网云服务器的点对点代理例子.代 ...
- 一种基于uCos-II操作系统和lwIP协议栈的IEEE-1588主站以及基于该主站的报文处理方法
主站以及应用于电力系统的支持IEEE‐1588协议的主时钟(IEEE‐1588主站)的实现方法.该方法是在一个低成本的硬件平台上,借助uCos‐II操作系统和TCP/IP的协议栈,对以太网数据进行了分 ...
- 【Qt编程】基于Qt的词典开发系列<十四>自动补全功能
最近写了一个查单词的类似有道词典的软件,里面就有一个自动补全功能(即当你输入一个字母时,就会出现几个候选项).这个自动补全功能十分常见,百度搜索关键词时就会出现.不过它们这些补全功能都是与你输入的进行 ...
- 基于Qt有限状态机的一种实现方式和完善的人工智能方法
基于Qt有限状态机的一种实现方式和完善的人工智能方法 人工智能在今年是一个非常火的方向,当然了.不不过今年,它一直火了非常多年,有关人工智能的一些算法层出不穷.人工智能在非常多领域都有应用,就拿我熟悉 ...
随机推荐
- 关于safari上的select宽高问题小技,自定义下拉框
之前一直用windows做开发,最近换了个mac,在几经折腾之下,安装完了各种开发工具,IDE等,然后欣然打开自己正在开发的网站.突然发现mac上所有的下拉框都变了,都是默认样式,无论padding, ...
- VS2008 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。
提示这个错误,自己的程序是在VS2008下编译的C/C++ win32程序,自己当时在win7上开发测试,都没有问题,正常使用,也在另一台xp系统上也试了,都没有问题.就发给客户了,没想到有些客户竟然 ...
- linux TIME_WAIT过多的解决方法
查看TCP状态:netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'查看SOCKET状态:cat /proc/n ...
- [Hapi.js] Using the response object
When you use reply method: let resp = reply('hello world') It actually return an response object. By ...
- java判断数据类型两种方式
instanceof String s = ""; System.out.println(s instanceof String); // true simp ...
- 用phantomjs 进行网页整页截屏
写截取整个网页程序是一个做前台的哥们所托,要做一些漂亮的界面原形,参考一些不错的网站设计就帮他弄了个截屏的程序. phantomjs 是一个基于js的webkit内核无头浏览器 也就是没有显示界面 ...
- js中的eval方法转换对象时,为何一定要加上括号?
待转换的是一个Json字符串: {'name':'新欢'} 而使用如下这种方式调用则会抛出语法异常, eval("{'name':'新欢'}"); 必须加上括号才行 eval(&q ...
- WCF入门教程系列二
一.概述 WCF能够建立一个跨平台的安全.可信赖.事务性的解决方案,是一个WebService,.Net Remoting,Enterprise Service,WSE,MSMQ的并集,有一副很经典的 ...
- git删除分支
git branch -d branchname删除一个分支需要具备的条件: 1 如果待删除的分支没有upstream branch,那么待删除的分支需要合并到HEAD上,否则需要使用-D强制删除 2 ...
- MySQL行级锁、表级锁、页级锁详细介绍
原文链接:http://www.jb51.net/article/50047.htm 页级:引擎 BDB.表级:引擎 MyISAM , 理解为锁住整个表,可以同时读,写不行行级:引擎 INNODB , ...