一种基于Qt的可伸缩的全异步C/S架构服务器实现(六) 整合各个模块实现功能

2023:

随着对Qt 信号-槽的认识加深,  在这里提醒读者该项目使用信号-槽进行基础 IO的做法是低效的,切勿模仿。该项目可作为一种不好的反面教材,演示了信号-槽的滥用。

可直接从 https://gitcode.com/colorEagleStdio/zoompipeline下载

       

在前面的章节中,介绍了网络传输、任务线程池、数据库和集群四个主要功能模块。到现在为止,这些模块都还只是一种资源,没有产生实际的运行效果。对一个具备真实功能的应用来说,需要有一个整合的过程。整合方法很多,这里以典型的客户 -客户通信来举例说明。

(一)类结构

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) 提供负载均衡建议, 在本服务器满员后,建议客户端到集群内最空闲的服务器进程登录。

这些类的合作关系如下:

SouthEast

(二)  客户端哈希

       客户端哈希的目的是为了迅速通过用户名、套接字找到对应的套接字和客户端对象。他们在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类构造函数中注册了本应用层实现类的工厂,这个工厂方法被用于产生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.cpp
zp_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.cpp
st_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_H

namespace 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 0x4567
			unsigned __int16 version; //Structure Version
			unsigned __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框架。

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Qt学生管理系统是一款基于C/S(Client/Server)架构的软件。C/S架构一种传统的软件架构模式,它采用客户端和服务器之间的分布式模式进行通信,其中客户端负责用户界面的展示和交互,服务器则处理客户端请求并存储数据。 在Qt学生管理系统中,客户端是以桌面应用的形式呈现的,它提供了用户友好的界面,使学生、教师和管理员可以方便地进行操作。客户端通过与服务器建立网络连接来传输数据和请求,因此,在C/S架构中,客户端和服务器之间的通信主要是通过网络实现的。 服务器端负责处理客户端发送过来的请求,包括对学生信息的增加、删除、修改以及查询等操作。服务器还负责存储学生信息的数据库,并且对数据进行管理和维护。通过使用Qt的网络模块服务器可以监听来自客户端的连接请求,并在连接建立后接收和处理客户端的请求。 使用C/S架构Qt学生管理系统具有很多优势。首先,由于客户端和服务器分别完成不同的功能,使得系统可以更好地进行模块化设计和开发,提高了系统的可维护性和可扩展性。其次,C/S架构可以实现客户端与服务器之间的分布式部署,提高了系统的并发性和响应速度,可以满足多用户同时访问系统的需求。此外,使用C/S架构还可以实现数据的集中管理和备份,确保了数据的安性和稳定性。 总的来说,Qt学生管理系统采用C/S架构,充分发挥了客户端和服务器的特点,实现了学生信息的管理和分布式处理,提高了系统的性能和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁劲犇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值