二十一、服务端多线程分组处理多客户端

前言

一、生产者与消费者设计模式

  • 某个模块负责产生数据,这些数据由另一个模块来负责处理:
    • 产生数据的模块,就形象地称为生产者
    • 而处理数据的模块,就称为消费者
  • 该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据
  • 缓冲区的作用:
    • 1.解耦,生产者和消费者只依赖缓冲区,而不相互依赖
    • 2.支持并发和异步

二、服务端模型图

在这里插入图片描述

三、服务端代码优化

1、分离客户端连接与消息处理业务

  • 原先的代码是将每个客户端保存在EasyTcpServer的数组中,并且在EasyTcpServer::Onrun()函数中调用select对每个客户端进行数据监听
  • 现在的代码为:
    • 保持EasyTcpServer不变,作为生产者,其内部创建一个CellServer对象数组
    • 但是客户端不在EasyTcpServer内存储了,创建一个新的class CellServer,作为消费者,其保存客户端的连接,并在CellServer::Onrun()函数中调用select对每个客户端进行数据监听
    • EasyTcpServer只接收客户端的连接,接收到客户端的连接之后,将其传递给自己CellServer对象数组中的某一个CellServer对象进行管理

2、为消息处理线程添加新客户端缓冲队列

  • 新增一个class CellServer
    • 作为消费者,从缓冲区中查看是否有新客户端,如果有那么就将新客户端添加到自己的fd_set中进行select监听
    • 新增一个addClient方法,用来向缓冲队列中添加新的客户端
class CellServer
{
public:
	CellServer(SOCKET sock = INVALID_SOCKET)
	{
		_sock = sock;
	}

	~CellServer()
	{
		Close();
		_sock = INVALID_SOCKET;
	}

	SOCKET _maxSock;
	bool OnRun()
	{
		_clients_change = true;
		while (isRun())
		{
			if (_clientsBuff.size() > 0)
			{//从缓冲队列里取出客户数据
				std::lock_guard<std::mutex> lock(_mutex);
				for (auto pClient : _clientsBuff)
				{
					_clients[pClient->sockfd()] = pClient;
				}
				_clientsBuff.clear();
				_clients_change = true;
			}
			//略。。。
		}
	}
	
	void addClient(ClientSocket* pClient)
	{
		std::lock_guard<std::mutex> lock(_mutex);
		//_mutex.lock();
		_clientsBuff.push_back(pClient);
		//_mutex.unlock();
	}
private:
	SOCKET _sock;
	//正式客户队列
	std::map<SOCKET, ClientSocket*> _clients;
	//缓冲客户队列
	std::vector<ClientSocket*> _clientsBuff;
	//缓冲队列的锁
	std::mutex _mutex;
};

3、建立消息处理线程

  • EasyTcpServer在EasyTcpServer::Start()函数中创建指定数量的CellServer消费者对象,然后调用CellServer.Start()函数
  • 每个CellServer对象的Start()函数创建一个线程,线程的执行函数为自己的OnRun()函数
  • OnRun()函数中调用select对每个客户端进行数据监听

a)EasyTcpServer::Start()

	void Start(int nCellServer)
	{
		for (int n = 0; n < nCellServer; n++)
		{
			auto ser = new CellServer(_sock);
			_cellServers.push_back(ser);
			//启动消息处理线程
			ser->Start();
		}
	}

b)CellServer::Start()

	void Start()
	{
		_thread = std::thread(std::mem_fn(&CellServer::OnRun), this);
	}

4、将新客户端分配给客户端数量最少的消息线程

  • EasyTcpServer::Accept()接收到一个新的客户端之后,调用EasyTcpServer::addClientToCellServer()将新的客户端加入到CellServer的_clientsBuff缓冲队列中
  • 放入到CellServer的_clientsBuff缓冲队列中的时候,通过调用每个CellServer对象的getClientCount()函数,如果哪个CellServer对象所管理的客户端最少,那么就将这个新的客户端放入到哪个CellServer对象的_clientsBuff缓冲队列中

a)CellServer::getClientCount

	size_t getClientCount()
	{
		return _clients.size() + _clientsBuff.size();
	}

b)EasyTcpServer::Accept

	//接受客户端连接
	SOCKET Accept()
	{
		//略。。。
		if (INVALID_SOCKET == cSock)
		{
			printf("socket=<%d>错误,接受到无效客户端SOCKET...\n", (int)_sock);
		}
		else
		{
			//将新客户端分配给客户数量最少的cellServer
			addClientToCellServer(new ClientSocket(cSock));
			//获取IP地址 inet_ntoa(clientAddr.sin_addr)
		}
		return cSock;
	}

c)EasyTcpServer::addClientToCellServer

	void addClientToCellServer(ClientSocket* pClient)
	{
		_clients.push_back(pClient);
		//查找客户数量最少的CellServer消息处理对象
		auto pMinServer = _cellServers[0];
		for (auto pCellServer : _cellServers)
		{
			if (pMinServer->getClientCount() > pCellServer->getClientCount())
			{
				pMinServer = pCellServer;
			}
		}
		pMinServer->addClient(pClient);
	}

5、消息处理线程在无客户端时休眠1毫秒

  • CellServer::OnRun()中如果_clients为空,那么说明无客户端,那么休眠一秒继续运行
    • 注意这里不要直接使用Sleep,应该使用标准库提供的休眠方法,这样才可以跨平台
bool OnRun()
	{
		_clients_change = true;
		while (isRun())
		{
			if (_clientsBuff.size() > 0)
			{//从缓冲队列里取出客户数据
				std::lock_guard<std::mutex> lock(_mutex);
				for (auto pClient : _clientsBuff)
				{
					_clients[pClient->sockfd()] = pClient;
				}
				_clientsBuff.clear();
				_clients_change = true;
			}

			//如果没有需要处理的客户端,就跳过
			if (_clients.empty())
			{
				std::chrono::milliseconds t(1);
				std::this_thread::sleep_for(t);
				continue;
			}
			//略。。。
			//伯克利套接字 BSD socket

6、为消息处理线程添加每秒收包计数

  • 在EasyTcpServer中定义一个_recvCount变量,用来表示服务端接收到客户端数据包的数量
  • _recvCount在EasyTcpServer::OnNetMsg()函数中被++
  • CellServer::OnNetMsg()函数每收到一次消息,那么调用其绑定的EasyTcpServer对象的OnNetMsg()函数,将_recvCount++
  • EasyTcpServer::Onrun()函数每执行一次,调用一次EasyTcpServer::time4msg()函数,EasyTcpServer::time4msg()函数统计_recvCount的信息并打印
	//计算并输出每秒收到的网络消息
	void time4msg()
	{
		auto t1 = _tTime.getElapsedSecond();
		if (t1 >= 1.0)
		{
			printf("thread<%d>,time<%lf>,socket<%d>,clients<%d>,recvCount<%d>\n", _cellServers.size(), t1, _sock, (int)_clientCount, (int)(_recvCount / t1));
			_recvCount = 0;
			_tTime.update();
		}
	}
	virtual void OnNetMsg(ClientSocket* pClient, DataHeader* header)
	{
		_recvCount++;
	}

7、事件通知,有客户端退出

a)client连接时增加一个延迟

void sendThread(int id)
{
	//略。。。
	for (int n = begin; n < end; n++)
	{
		client[n]->Connect("127.0.0.1", 4567);
		//client[n]->Connect("192.168.124.9", 4567);
		printf("thread<%d>,Connect=%d\n", id, n);
	}

	std::chrono::milliseconds t(5000);
	std::this_thread::sleep_for(t);
	//略。。。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无休止符

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

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

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

打赏作者

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

抵扣说明:

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

余额充值