一種基於Qt的可伸縮的全非同步C/S架構伺服器實現(六) 整合各個模組實現功能

來源:互聯網
上載者:User

標籤:c++   qt   伺服器   叢集   多線程   

        在前面的章節中,介紹了網路傳輸、任務線程池、資料庫和叢集四個主要功能模組。到現在為止,這些模組都還只是一種資源,沒有產生實際的運行效果。對一個具備真實功能的應用來說,需要有一個整合的過程。整合方法很多,這裡以典型的客戶 -客戶通訊來舉例說明。

(一)類結構

1、“用戶端” 這個概念被抽象為一個節點類st_clientNode,每個用戶端串連對應了該類的一個執行個體。這個類不但儲存了有關該串連的所有背景資訊(比如聊天程式中的使用者名稱等),還提供了正確解釋資料流的代碼實現。由於想分開傳輸層和應用程式層的代碼,實際例子中,該類被分為基礎傳輸類st_clientNode_baseTrans和應用程式層類st_clientNodeAppLayer兩部分。

2、 對沒有在本伺服器登入的用戶端來說,通過叢集的伺服器-伺服器傳輸鏈路實現跨伺服器通訊。“其他伺服器”這個概念被抽象為一個節點類st_cross_svr_node,叢集內每個伺服器串連對應了該類的一個執行個體。這個類不但儲存了有關該串連的所有背景資訊(比如伺服器的名字、地址等),還提供了正確解釋資料流的代碼實現。該類實現了三種叢集訊息類型。


    (1) 新用戶端登入廣播,用於通知叢集內所有伺服器,有新用戶端登入

    (2) 用戶端退出廣播。

    (3) 用戶端資料流打包傳輸

3、在最上層,有一個本伺服器處理序的管理者,稱作st_client_table,用於封裝所有的服務功能。這個類在每個伺服器處理序中僅有一個執行個體。它主要的工作有:

    (1) 提供一個盛放、管理各個用戶端節點類(st_clientNodeAppLayer執行個體)、各個叢集伺服器節點類(st_cross_svr_node執行個體)的容器;

    (2) 提供一個管理本地用戶端雜湊表的執行者

    (3) 提供一個管理全域用戶端雜湊表的執行者

    (4) 把網路傳輸、任務線程池、資料庫和叢集四個主要功能模組的訊號、槽全部關聯起來

    (5) 提供負載平衡建議, 在本伺服器滿員後,建議用戶端到叢集內最閒置伺服器處理序登入。


這些類的合作關係如下:

(二)  用戶端雜湊

       用戶端雜湊的目的是為了迅速通過使用者名稱、通訊端找到對應的通訊端和用戶端對象。他們在st_client_table類中定義。為了在較大規模下,仍然獲得較好的效率,使用了STL的 unordered_map類。使用基於樹的map、QMap也是可以的,但是訪問效率不如雜湊快。

//st_client_table 本地用戶端Hash 成員QMutex m_hash_mutex;std::unordered_map<quint32,st_clientNode_baseTrans *> m_hash_uuid2node;std::unordered_map<QObject *,st_clientNode_baseTrans *> m_hash_sock2node;//st_client_table 遠程用戶端Hash 成員std::unordered_map<quint32,QString> m_hash_remoteClient2SvrName;QMutex m_mutex_cross_svr_map;

*本地雜湊成員儲存了從使用者UUID(相當於使用者的全域標示)到使用者節點的映射。使用UUID可迅速得到節點指標,從而執行發送資料、踢出節點、修改資訊等功能

*本地雜湊成員儲存了從通訊端到使用者節點的映射。使用通訊端可迅速得到節點指標。這樣,在收到資料時,可直接定位到使用者節點。之所以沒有從QTcpSocket派生一個子類,存放從通訊端到使用者節點的映射,是因為通訊端的生存周期控制複雜,用戶端頻繁的登入、登出,一旦通訊端對象刪除失效,程式中其他位置使用通訊端指標直接擷取用戶端節點指標的操作就會溢出。

*遠程雜湊成員儲存了從用戶端 UUID到叢集伺服器名稱的映射。用於向遠程用戶端發送資料使用。

 (三) 溝通訊號與槽

      為了把本文涉及的所有模組溝通起來,st_client_table類做了很多工作。

      首先,網路模組、叢集會發出訊號,他們由st_client_table類響應並處理或移交。

//串連新使用者來到訊號,將分配新的使用者節點、登記雜湊表(對一般的Sock)、廣播叢集資訊connect (m_pThreadEngine,&ZPNetwork::zp_net_Engine::evt_NewClientConnected,this,&st_client_table::on_evt_NewClientConnected,Qt::QueuedConnection);//串連新使用者已保護訊號,將分配新的使用者節點、登記雜湊表(對SSL Sock)connect (m_pThreadEngine,&ZPNetwork::zp_net_Engine::evt_ClientEncrypted,this,&st_client_table::on_evt_ClientEncrypted,Qt::QueuedConnection);//串連新使用者斷開訊號,將清除雜湊表、廣播叢集資訊connect (m_pThreadEngine,&ZPNetwork::zp_net_Engine::evt_ClientDisconnected,this,&st_client_table::on_evt_ClientDisconnected,Qt::QueuedConnection);//串連使用者資料到來訊號,將直接向各個用戶端的處理隊列中push ,以待線程池解譯用戶端訊息connect (m_pThreadEngine,&ZPNetwork::zp_net_Engine::evt_Data_recieved,this,&st_client_table::on_evt_Data_recieved,Qt::QueuedConnection);//串連使用者資料成功發送訊號,目前只用於統計流量connect (m_pThreadEngine,&ZPNetwork::zp_net_Engine::evt_Data_transferred,this,&st_client_table::on_evt_Data_transferred,Qt::QueuedConnection);//串連新叢集節點接入訊號,將向該節點發送HELLO包,並交換持有的用戶端情況connect (m_pCluster,&ZP_Cluster::zp_ClusterTerm::evt_NewSvrConnected,this,&st_client_table::on_evt_NewSvrConnected,Qt::QueuedConnection);//串連叢集節點斷開訊號,將清除屬於該節點的用戶端全域雜湊connect (m_pCluster,&ZP_Cluster::zp_ClusterTerm::evt_NewSvrDisconnected,this,&st_client_table::on_evt_NewSvrDisconnected,Qt::QueuedConnection);//叢集節點資料接收,將直接向各個叢集節點的處理隊列中push ,以待線程池解譯叢集訊息connect (m_pCluster,&ZP_Cluster::zp_ClusterTerm::evt_RemoteData_recieved,this,&st_client_table::on_evt_RemoteData_recieved,Qt::QueuedConnection);//叢集節點資料發送, 只用於流量統計connect (m_pCluster,&ZP_Cluster::zp_ClusterTerm::evt_RemoteData_transferred,this,&st_client_table::on_evt_RemoteData_transferred,Qt::QueuedConnection);

   
接著,在各個新用戶端接入的時候,建立屬於該用戶端的節點類對象,並建立訊號串連。

//this event indicates new client connected.void  st_client_table::on_evt_NewClientConnected(QObject * clientHandle){bool nHashContains = false;st_clientNode_baseTrans * pClientNode = 0;m_hash_mutex.lock();nHashContains = (m_hash_sock2node.find(clientHandle)!=m_hash_sock2node.end())?true:false;if (false==nHashContains){st_clientNode_baseTrans * pnode = new st_clientNodeAppLayer(this,clientHandle,0);//using queued connection of send and revieve;connect (pnode,&st_clientNode_baseTrans::evt_SendDataToClient,m_pThreadEngine,&ZPNetwork::zp_net_Engine::SendDataToClient,Qt::QueuedConnection);connect (pnode,&st_clientNode_baseTrans::evt_close_client,m_pThreadEngine,&ZPNetwork::zp_net_Engine::KickClients,Qt::QueuedConnection);connect (pnode,&st_clientNode_baseTrans::evt_Message,this,&st_client_table::evt_Message,Qt::QueuedConnection);m_hash_sock2node[clientHandle] = pnode;nHashContains = true;pClientNode = pnode;}else{pClientNode =  m_hash_sock2node[clientHandle];}m_hash_mutex.unlock();assert(nHashContains!=0 && pClientNode !=0);}
上面的代碼中串連了三組訊號,第一組把用戶端發送資料的訊號與網路模組串連起來,如用戶端A發出該訊號,含有用戶端B的標示,將由網路模組響應,並最終發給用戶端B。第二組串連了用戶端踢出訊號,比如用戶端A想踢出用戶端B,則把B的標示泵出,由網路模組響應並踢出B。第三組串連了訊息顯示訊號。

(四) 叢集應用程式層實現

      

        前述,叢集模組只提供了各個叢集節點的物理串連,並沒有實現具體的功能。因此,作為一個特定的應用,需要在叢集模組上實現一個應用程式層功能。這個應用程式層功能有兩部分組成。

      一個是叢集的應用程式層實作類別st_cross_svr_node, 由叢集節點類ZP_Cluster::zp_ClusterNode派生,負責信令解譯。    為了讓叢集知道這個類的存在,並使用該類而不是基類作為節點類,在st_client_table類建構函式中註冊了本應用程式層實作類別的工廠,這個Factory 方法被用於產生st_cross_svr_node類的執行個體。

//zp_clusterterm.h//.../** * The factory enables user-defined sub-classes inherits from zp_ClusterNode * Using SetNodeFactory , set your own allocate method. *註冊工廠的方法/void SetNodeFactory(std::function<zp_ClusterNode * (zp_ClusterTerm * /*pTerm*/,QObject * /*psock*/,QObject * /*parent*/)>);std::function<zp_ClusterNode * (zp_ClusterTerm * /*pTerm*/,QObject * /*psock*/,QObject * /*parent*/)> m_factory; //zp_clusterterm.cppzp_ClusterTerm::zp_ClusterTerm(/*...*/ ) {//...m_factory = std::bind(&zp_ClusterTerm::default_factory,this,_1,_2,_3);}/** * @brief The factory enables user-defined sub-classes inherits from zp_ClusterNode * Using SetNodeFactory , set your own allocate method. * @fn zp_ClusterTerm::default_factory the default factory function. just return zp_ClusterTerm * * @param pTerm Term object * @param psock Sock Object * @param parent Parent * @return zp_ClusterNode * 預設工廠產生zp_ClusterNode 類的執行個體 */zp_ClusterNode * zp_ClusterTerm::default_factory(zp_ClusterTerm * pTerm,QObject * psock,QObject * parent){return new zp_ClusterNode(pTerm,psock,parent);}//st_client_table.cppst_client_table::st_client_table(...) {m_pCluster->SetNodeFactory(std::bind(&st_client_table::cross_svr_node_factory,  this,  _1,_2,_3));//綁定工廠回調方法}ZP_Cluster::zp_ClusterNode * st_client_table::cross_svr_node_factory(ZP_Cluster::zp_ClusterTerm * pTerm,QObject * psock,QObject * parent){st_cross_svr_node * pNode = new st_cross_svr_node(pTerm,psock,parent);pNode->setClientTable(this);return pNode;//實際產生的是st_cross_svr_node類的執行個體}
   

另一個是叢集的應用程式層訊息,如下:


#ifndef ST_CROSS_SVR_MSG_H#define ST_CROSS_SVR_MSG_Hnamespace ExampleServer{#pragma  pack (push,1)#if defined(__GNUC__)#include <stdint.h>typedef struct tag_example_crosssvr_msg{struct tag_msgHearder{__UINT16_TYPE__ Mark;    //Always be "0x4567"__UINT16_TYPE__ version; //Structure Version__UINT8_TYPE__ mesageType;__UINT32_TYPE__ messageLen;} header;union union_payload{__UINT8_TYPE__ data[1];__UINT32_TYPE__ uuids[1];} payload;} EXAMPLE_CROSSSVR_MSG;#endif#if defined(_MSC_VER)typedef struct tag_example_crosssvr_msg{struct tag_msgHearder{unsigned __int16 Mark;    //Always be 0x4567unsigned __int16 version; //Structure Versionunsigned __int8 mesageType;unsigned __int32 messageLen;} header;union union_payload{unsigned __int8 data[1];unsigned __int32 uuids[1];} payload;} EXAMPLE_CROSSSVR_MSG;#endif#pragma pack(pop)}#endif

基於上述的資料結構和處理方法,實現了三個messageType訊息類型。

0x01 為 遠程用戶端登入訊息,payload為各個新登入到該伺服器用戶端的UUID

0x02 為 遠程用戶端退出訊息,payload為各個從該伺服器退出的用戶端的UUID

0x03 為 用戶端載荷訊息,封裝了從遠程用戶端發給本地用戶端之間的用戶端-用戶端訊息。

(五) 均衡建議

    無中心的伺服器叢集通過心跳廣播在各個節點上儲存了當前所有節點的負荷。當用戶端登入時,會首先檢查當前伺服器負荷是否超過門限,一旦超過,則觸發均衡建議。

  

bool st_client_table::NeedRedirect(quint8 bufAddresses[/*64*/],quint16 * pnPort){if (m_pCluster->clientNums()<m_nBalanceMax)return false;QString strServerName = m_pCluster->minPayloadServer(bufAddresses,pnPort);if (strServerName==m_pCluster->name())return false;return true;}bool st_clientNodeAppLayer::LoginClient(){//...stMsg_ClientLoginRsp & reply = new stMsg_ClientLoginRsp (...);//...//Cluster-Balance.if (m_pClientTable->NeedRedirect(reply.Address_Redirect,&reply.port_Redirect)){reply.DoneCode = 1;//}        //...emit evt_SendDataToClient(this->sock(),reply);}

用戶端根據建議,可選擇重新按新地址串連,或者繼續保持。


結語

        C/S 架構伺服器實現方式很多,應用案例成千上萬。本範例為了示範基本的知識點,採用的設計思路並不是單純從效能出發的。基於Qt實現,有助於利用Qt本身的引用計數、跨平台等特性,同時,Qt也是封裝的非常棒的庫,使得我們可以拋開繁瑣的API,直接研究問題本身,為Linux, Windows下的同學提供統一的參考範例。感謝為Qt的進步付出心血的貢獻者們,他們的不懈堅持讓C++語言擁有了一個完整的跨平台UI架構。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.