一种基于Qt的可伸缩的全异步C/S架构服务器实现(二) 网络传输

转自https://blog.csdn.net/goldenhawking/article/details/26095215

二、网络传输模块

模块对应代码命名空间    (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对象。核心代码如下:


 
 
  1. m_tcpServer = new ZP_TcpServer( this);
  2. 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派生。它本身不是一个线程对象,而是被“绑定”在一个线程对象中运行的。一个进程可以拥有若干监听端口,这些监听端口对应了不同的zp_netListenThread对象。这些监听线程对象由zp_net_Engine类管理,存储在这个类的成员变量中。下面两个成员变量

 
 
  1. //This map stores listenThreadObjects
  2. QMap<QString,zp_netListenThread *> m_map_netListenThreads;
  3. //Internal Threads to hold each listenThreadObjects' message Queue
  4. QMap<QString,QThread *> m_map_netInternalListenThreads;
第一个存储了各个端口的线程对象,第二个存储了各个端口的线程。
由于具体下达监听任务的线程是主线程(UI),但执行任务的线程是工作线程,所以,所有的指令均不是通过直接的函数调用来实现,取而代之的是使用Qt的信号与槽。比如,UI按钮被点击,则触发了startListen 信号,转而由zp_netListenThread的startListen槽来响应。这里需要注意的是,由于Qt的信号与槽系统是一种广播系统,意味着一个zp_net_Engine类管理多个zp_netListenThread对象时,zp_net_Engine发出的信号会被所有zp_netListenThread对象接收。因此,信号与槽中含有一个唯一标示,用于指示本次信号触发是为了操作具体哪个对象。这种技术在类似的场合被多次使用。

 
 
  1. void zp_net_Engine::AddListeningAddress(QString id, const QHostAddress & address , quint16 nPort, bool bSSLConn /*= true*/)
  2. {
  3. if (m_map_netListenThreads.find(id)==m_map_netListenThreads.end())
  4. {
  5. //Start Thread
  6. QThread * pThread = new QThread( this);
  7. zp_netListenThread * pListenObj = new zp_netListenThread(id,address,nPort,bSSLConn);
  8. pThread->start();
  9. //m_mutex_listen.lock();
  10. m_map_netInternalListenThreads[id] = pThread;
  11. m_map_netListenThreads[id] = pListenObj;
  12. //m_mutex_listen.unlock();
  13. //Bind Object to New thread
  14. connect( this,&zp_net_Engine::startListen,pListenObj,&zp_netListenThread::startListen,Qt::QueuedConnection);
  15. connect( this,&zp_net_Engine::stopListen,pListenObj,&zp_netListenThread::stopListen,Qt::QueuedConnection);
  16. connect(pListenObj,&zp_netListenThread::evt_Message, this,&zp_net_Engine::evt_Message,Qt::QueuedConnection);
  17. connect(pListenObj,&zp_netListenThread::evt_ListenClosed, this,&zp_net_Engine::on_ListenClosed,Qt::QueuedConnection);
  18. connect(pListenObj,&zp_netListenThread::evt_NewClientArrived, this,&zp_net_Engine::on_New_Arrived_Client,Qt::QueuedConnection);
  19. pListenObj->moveToThread(pThread);
  20. //Start Listen Immediately
  21. emit startListen(id);
  22. }
  23. else
  24. emit evt_Message( this, "Warning>"+QString(tr( "This ID has been used.")));
  25. }


2.2.2 接受连接过程

客户端发起接入请求后,首先触发了ZP_TcpServer的incomingConnection方法。在下面这个方法中,套接字的描述符作为事件的参数被泵出。


 
 
  1. void ZP_TcpServer::incomingConnection(qintptr socketDescriptor)
  2. {
  3. emit evt_NewClientArrived(socketDescriptor);
  4. }
    上面的信号对应的槽为zp_net_Engine::on_New_Arrived_Client槽函数。在这个函数中,网络模块首先从当前可用的传输线程中确定最空闲的那个线程,而后把套接字描述符转交给传输线程。这个部分的核心代码:

 
 
  1. void zp_net_Engine::on_New_Arrived_Client(qintptr socketDescriptor)
  2. {
  3. zp_netListenThread * pSource = qobject_cast<zp_netListenThread *>(sender());
  4. if (!pSource)
  5. {
  6. emit evt_Message(this,"Warning>"+QString(tr("Non-zp_netListenThread type detected.")));
  7. return;
  8. }
  9. emit evt_Message(this,"Info>" + QString(tr("Incomming client arriverd.")));
  10. int nsz = m_vec_NetTransThreads.size();
  11. int nMinPay = 0x7fffffff;
  12. int nMinIdx = -1;
  13. for ( int i= 0;i<nsz && nMinPay!= 0;i++)
  14. {
  15. if (m_vec_NetTransThreads[i]->isActive()== false ||
  16. m_vec_NetTransThreads[i]->SSLConnection()!=pSource->bSSLConn()
  17. )
  18. continue;
  19. int nPat = m_vec_NetTransThreads[i]->CurrentClients();
  20. if (nPat<nMinPay)
  21. {
  22. nMinPay = nPat;
  23. nMinIdx = i;
  24. }
  25. //qDebug()<<i<<" "<<nPat<<" "<<nMinIdx;
  26. }
  27. //...
  28. if (nMinIdx>= 0 && nMinIdx<nsz)
  29. emit evt_EstablishConnection(m_vec_NetTransThreads[nMinIdx],socketDescriptor);
  30. else
  31. {
  32. emit evt_Message( this, "Warning>"+QString(tr( "Need Trans Thread Object for clients.")));
  33. }
  34. }
上面的代码中, evt_EstablishConnection 事件携带了由均衡策略确定的承接线程、socketDescriptor 描述符。这个事件广播给所有的传输线程对象。在各个对象的incomingConnection槽中,具体生成用于传输的套接字对象.注意, 这个槽函数是运行在各个传输线程的事件循环中的,因此,创建的套接字直接属于特定线程.


 
 
  1. /**
  2. * @brief This slot dealing with multi-thread client socket accept.
  3. * accepy works start from zp_netListenThread::m_tcpserver, end with this method.
  4. * the socketDescriptor is delivered from zp_netListenThread(a Listening thread)
  5. * to zp_net_Engine(Normally in main-gui thread), and then zp_netTransThread.
  6. *
  7. * @param threadid if threadid is not equal to this object, this message is just omitted.
  8. * @param socketDescriptor socketDescriptor for incomming client.
  9. */
  10. void zp_netTransThread::incomingConnection(QObject * threadid,qintptr socketDescriptor)
  11. {
  12. if (threadid!= this)
  13. return;
  14. QTcpSocket * sock_client = 0;
  15. if (m_bSSLConnection)
  16. sock_client = new QSslSocket( this);
  17. else
  18. sock_client = new QTcpSocket( this);
  19. if (sock_client)
  20. {
  21. //Initial content
  22. if ( true ==sock_client->setSocketDescriptor(socketDescriptor))
  23. {
  24. connect(sock_client, &QTcpSocket::readyRead, this, &zp_netTransThread::new_data_recieved,Qt::QueuedConnection);
  25. connect(sock_client, &QTcpSocket::disconnected, this,&zp_netTransThread::client_closed,Qt::QueuedConnection);
  26. connect(sock_client, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError)),Qt::QueuedConnection);
  27. connect(sock_client, &QTcpSocket::bytesWritten, this, &zp_netTransThread::some_data_sended,Qt::QueuedConnection);
  28. m_mutex_protect.lock();
  29. m_clientList[sock_client] = 0;
  30. m_mutex_protect.unlock();
  31. if (m_bSSLConnection)
  32. {
  33. QSslSocket * psslsock = qobject_cast<QSslSocket *>(sock_client);
  34. assert(psslsock!= NULL);
  35. QString strCerPath = QCoreApplication::applicationDirPath() + "/svr_cert.pem";
  36. QString strPkPath = QCoreApplication::applicationDirPath() + "/svr_privkey.pem";
  37. psslsock->setLocalCertificate(strCerPath);
  38. psslsock->setPrivateKey(strPkPath);
  39. connect(psslsock, &QSslSocket::encrypted, this, &zp_netTransThread::on_encrypted,Qt::QueuedConnection);
  40. psslsock->startServerEncryption();
  41. }
  42. emit evt_NewClientConnected(sock_client);
  43. emit evt_Message(sock_client,"Info>" + QString(tr("Client Accepted.")));
  44. }
  45. else
  46. sock_client->deleteLater();
  47. }
  48. }

2.2.3 数据接收

在成功创建了套接字后, 数据的收发都在传输线程中运行了.当套接字收到数据后,简单的触发事件

evt_Data_recieved


 
 
  1. void zp_netTransThread::new_data_recieved()
  2. {
  3. QTcpSocket * pSock = qobject_cast<QTcpSocket*>(sender());
  4. if (pSock)
  5. {
  6. QByteArray array = pSock->readAll();
  7. int sz = array.size();
  8. g_mutex_sta.lock();
  9. g_bytesRecieved +=sz;
  10. g_secRecieved += sz;
  11. g_mutex_sta.unlock();
  12. emit evt_Data_recieved(pSock,array);
  13. }
  14. }

2.2.4数据发送

尽管Qt的套接字本身具备缓存,塞入多大的数据都会成功, 但是本实现仍旧使用额外的队列, 每次缓存一个固定长度的片段并顺序发送. 这样的好处,是可以给代码使用者一个机会,来加入代码检查缓冲区的大小,并作一些持久化的工作. 比如,队列超过100MB后,就把后续的数据缓存在磁盘上, 而不是继续放在内存中,

实现这个策略的变量是两个缓存.


 
 
  1. //sending buffer, hold byteArraies.
  2. QMap<QObject *,QList<QByteArray> > m_buffer_sending;
  3. QMap<QObject *,QList<qint64> > m_buffer_sending_offset;

第一个缓存存储各个套接字的队列.另一个存储各个数据块的发送偏移. 这样做是有性能缺陷的, 更好的办法是从 QTcpSocket 派生自己的类,并把各个套接字的缓存直接存储在派生类实例中去. 在本实现中, 直接使用了 QTcpSocket和QSSLSocket类, 因而有一定的性能损失.

一个槽方法  SendDataToClient 负责接受发送数据的请求.


 
 
  1. void zp_netTransThread::SendDataToClient(QObject * objClient,QByteArray dtarray)
  2. {
  3. m_mutex_protect.lock();
  4. if (m_clientList.find(objClient)==m_clientList.end())
  5. {
  6. m_mutex_protect.unlock();
  7. return;
  8. }
  9. m_mutex_protect.unlock();
  10. QTcpSocket * pSock = qobject_cast<QTcpSocket*>(objClient);
  11. if (pSock&&dtarray.size())
  12. {
  13. QList<QByteArray> & list_sock_data = m_buffer_sending[pSock];
  14. QList<qint64> & list_offset = m_buffer_sending_offset[pSock];
  15. if (list_sock_data.empty()== true)
  16. {
  17. qint64 bytesWritten = pSock->write(dtarray.constData(),qMin(dtarray.size(),m_nPayLoad));
  18. if (bytesWritten < dtarray.size())
  19. {
  20. list_sock_data.push_back(dtarray);
  21. list_offset.push_back(bytesWritten);
  22. }
  23. }
  24. else
  25. {
  26. list_sock_data.push_back(dtarray);
  27. list_offset.push_back( 0);
  28. }
  29. }
  30. }

在上面的函数中,将检查队列是否为空.为空的话,将触发 QTcpSocket::write方法发出m_nPayload大小的数据块.当这些数据块发送完毕,将触发QTcpSocket::bytesWritten事件,由下面的槽响应.


 
 
  1. /**
  2. * @brief this slot will be called when internal socket successfully
  3. * sent some data. in this method, zp_netTransThread object will check
  4. * the sending-queue, and send more data to buffer.
  5. *
  6. * @param wsended
  7. */
  8. void zp_netTransThread::some_data_sended(qint64 wsended)
  9. {
  10. g_mutex_sta.lock();
  11. g_bytesSent +=wsended;
  12. g_secSent += wsended;
  13. g_mutex_sta.unlock();
  14. QTcpSocket * pSock = qobject_cast<QTcpSocket*>(sender());
  15. if (pSock)
  16. {
  17. emit evt_Data_transferred(pSock,wsended);
  18. QList<QByteArray> & list_sock_data = m_buffer_sending[pSock];
  19. QList<qint64> & list_offset = m_buffer_sending_offset[pSock];
  20. while (list_sock_data.empty()== false)
  21. {
  22. QByteArray & arraySending = *list_sock_data.begin();
  23. qint64 & currentOffset = *list_offset.begin();
  24. qint64 nTotalBytes = arraySending.size();
  25. assert(nTotalBytes>=currentOffset);
  26. qint64 nBytesWritten = pSock->write(arraySending.constData()+currentOffset,qMin(( int)(nTotalBytes-currentOffset),m_nPayLoad));
  27. currentOffset += nBytesWritten;
  28. if (currentOffset>=nTotalBytes)
  29. {
  30. list_offset.pop_front();
  31. list_sock_data.pop_front();
  32. }
  33. else
  34. break;
  35. }
  36. }
  37. }

2.2.5 其他工作

     在传输终止后, 会进行一定的清理. 对于多线程的传输,最重要的是确保各个对象的生存期. 有兴趣的读者可以使用 sharedptr来管理动态分配的对象, 这样操作起来会很方便. 在本范例中, 所有代码均进行了 7*24 调试.

    下一章,将介绍流水线线程池的原理和实现.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本资源设置1个资源分,您可以下载作为捐献。 如果您有Git,还可以从http://www.goldenhawking.org:3000/goldenhawking/zoom.pipeline直接签出最新版本 (上一个版本“一种伸缩异步C/S架构服务器实现”是有问题的,现在已经完成更改)。 服务由以下几个模块组成. 1、 网络传输模块。负责管理用于监听、传输的套接字,并控制数据流在不同线程中流动。数据收发由一定规模的线程池负责,实现方法完得益于Qt的线程事件循环。被绑定到某个Qthread上的Qobject对象,其信号-槽事件循环由该线程负责。这样,便可方便的指定某个套接字对象使用的线程。同样,受惠于Qt的良好封装,直接支持Tcp套接字及SSL套接字,且在运行时可动态调整。(注:编译这个模块需要Qt的SSL支持,即在 configure 时加入 -openssl 选项) 2、 任务流水线模块。负责数据的处理。在计算密集型的应用中,数据处理负荷较重,需要和网络传输划分开。基于普通线程池的处理模式,也存在队列阻塞的问题——若干个客户端请求的耗时操作,阻塞了其他客户端的响应,哪怕其他客户端的请求很短时间就能处理完毕,也必须排队等待。采用流水线线程池避免了这个问题。每个客户端把需要做的操作进行粒度化,在一个环形的队列中,线程池对单个客户端,每次仅处理一个粒度单位的任务。单个粒度单位完成后,该客户端的剩余任务便被重新插入到队列尾部。这个机制保证了客户端的整体延迟较小。 3、 服务集群管理模块。该模块使用了网络传输模块、任务流水线模块的功能,实现了跨进程的服务器ßà服务器链路。在高速局域网中,连接是快速、稳定的。因此,该模块被设计成一种星型无中心网络。任意新增服务器节点选择现有服务器集群中的任意一个节点,接入后,通过广播自动与其他服务器节点建立点对点连接。本模块只是提供一个服务器服务器的通信隧道,不负责具体通信内容的解译。对传输内容的控制,由具体应用决定。 4、 数据库管理模块。该模块基于Qt的插件式数据库封装QtSql。数据库被作为资源管理,支持在多线程的条件下,使用数据库资源。 5、 框架界面。尽管常见的服务运行时表现为一个后台进程,但为了更好的演示服务器的功能,避免繁琐的配置,还是需要一个图形界面来显示状态、设置参数。本范例中,界面负责轮训服务器的各个状态,并设置参数。设置好的参数被存储在一个ini文件中,并在服务开启时加载。 6、应用专有部分模块。上述1-4共四个主要模块均是通用的。他们互相之间没有形成联系,仅仅是作为一种资源存在于程序的运行时(Runtime)之中。应用专有部分模块根据具体任务需求,灵活的使用上述资源,以实现功能。在范例代码中,实现一种点对点的转发机制。演示者虚拟出一些工业设备,以及一些操作员使用的客户端软件。设备与客户端软件在成功认证并登录后,需要交换数据。改变这个模块的代码,即可实现自己的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值