一种基于Qt的可伸缩的全异步C/S架构服务器实现(二) 网络传输
来源:互联网 发布:手机网络有延迟怎么办 编辑:程序博客网 时间:2024/05/16 15:49
二、网络传输模块
模块对应代码命名空间 (namespace ZPNetwork)
模块对应代码存储文件夹 (\ZoomPipeline_FuncSvr\network)
2.1 模块结构
网络传输模块负责管理监听器,并根据各个传输线程目前的负荷,把新申请接入的客户套接字描述符引导到最空闲的传输线程中执行“接受连接(Accept)”操作。该模块由如下几个类组成。
1、zp_net_Engine类,派生自Qobject。模块的外部接口类,同时也是功能管理者。提供了设置监听器、配置线程池的功能。
2、zp_netListenThread类:派生自QObject。用于绑定在各个监听线程的事件循环中,不断的接受客户端连接请求。该类会在信号中把套接字描述符(socketdescriptor)泵出,由zp_net_Engine类进行负荷均衡,选取当前负荷最小的传输线程(zp_netTransThread)接受该接入申请。
3、zp_netTransThread类:派生自QObject。用于绑定在各个传输线程的事件循环中,具体承担数据传输。一个zp_netTransThread线程可以承担多个客户端的收发请求。
4、ZP_TcpServer类:派生自QtcpServer。重载了ZP_TcpServer::incomingConnection,不在监听线程中进行Accept操作,而是直接发出evt_NewClientArrived信号,把套接字描述符(socketdescriptor)泵出,由zp_net_Engine类进行负荷均衡,选取当前负荷最小的传输线程(zp_netTransThread)接受该接入申请。
这四个类的合作关系图如下
2.2 系统原理
为了提供基于线程池的TCP服务,zp_net_engine类有几个重要成员。下面,按照一次客户端发起连接的过程,逆向的逐一来介绍这些类的合作原理.
2.2.1 监听器与监听线程
1、监听器ZP_TcpServer
系统运行时,负责监听工作的是 QtcpServer 派生类,名称叫ZP_TcpServer。该类重载了 QtcpServer的incomingConnection()方法1。当网络中一个客户端发起连接时,这个函数会被立刻调用。在本派生类中,并没有直接产生套接字。它仅仅触发了一个称为“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);
3、操作监听器的模块接口类zp_net_Engine
zp_netListenThread类本身是从Qobject派生。它本身不是一个线程对象,而是被“绑定”在一个线程对象中运行的。一个进程可以拥有若干监听端口,这些监听端口对应了不同的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;
由于具体下达监听任务的线程是主线程(UI),但执行任务的线程是工作线程,所以,所有的指令均不是通过直接的函数调用来实现,取而代之的是使用Qt的信号与槽。比如,UI按钮被点击,则触发了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 接受连接过程
客户端发起接入请求后,首先触发了ZP_TcpServer的incomingConnection方法。在下面这个方法中,套接字的描述符作为事件的参数被泵出。
- void ZP_TcpServer::incomingConnection(qintptr socketDescriptor)
- {
- emit evt_NewClientArrived(socketDescriptor);
- }
- 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.")));
- }
- }
- /**
- * @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架构服务器实现(二) 网络传输
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(二) 网络传输
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(六) 整合各个模块实现功能
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(六) 整合各个模块实现功能
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(四)数据库的多线程操作
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(四)数据库的多线程操作
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(三) 流水线结构线程池设计
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(五) 单层无中心集群
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(三) 流水线结构线程池设计
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(五) 单层无中心集群
- 一种可展开伸缩的tableView实现
- 基于C/S架构的网络聊天室
- 一种B/S模式下基于JAVA反射机制的可伸缩动态加载模块的解决方案
- Linux C: 基于C/S的多线程网络编程 (服务器充当一个客户端的两点传输)
- Qt 多线程与数据库操作需要注意的几点问题
- LeetCode--Roman to Integer
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述
- 机器学习经典算法详解及Python实现--K近邻(KNN)算法
- 世上本没有对错,看问题的角度不同,答案不同而已,我们应该学会常常用别人的角度看世界,多一分宽容,多一分理解,多一分求同存异。
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(二) 网络传输
- jsp分页查询及显示
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(三) 流水线结构线程池设计
- 求多个数的最小公倍数
- CMAKE 和 MAKE 区别
- 【来日复制粘贴】使某列筛选的结果映射到标题部分
- 一种基于Qt的可伸缩的全异步C/S架构服务器实现(四)数据库的多线程操作
- CMake 使用方法
- 写留的商务超市管理系统~~~第二部分