C++网络编程学习:心跳机制与定时发送数据

网络编程学习记录

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows(本文中内容使用windows平台下vs2019开发,故本文项目不完全支持linux平台)

C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)  点我查看之前的代码开发记录


笔记十一

  • 网络编程学习记录
  • 0:本次增改内容
  • 1:更改客户端储存方式
  • 2:更改任务队列储存方式
  • 3:加入心跳检测机制
  • 4:加入定时发送缓存消息机制
  • 5:将内存池静态库分离
  • ※ - 项目源码 (github)

0:本次增改内容

  1. 更改服务端中,客户端对象储存的方式,由vector改为map。
  2. 改变任务队列中任务储存方式,由任务基类改为匿名函数。
  3. 加入心跳检测机制,及时剔除未响应客户端。
  4. 加入定时发送消息检测机制,及时发送缓冲区内的内容。
  5. 将内存池静态库分离,使客户端源码也可以引用。

1:更改客户端储存方式

  之前,我的服务端程序储存客户端对象ClientSocket的方式是std::vector,在select筛选后的fd_set中使用FD_ISSET函数获取需接收报文的客户端。
  但是FD_ISSET函数是使用for循环进行暴力检索,消耗较大,我们可以改为使用std::map::find进行检索。这样就需要把储存客户端对象的方式改为std::map。因为我们需要通过socket进行查找,所以我把std::map的键设为SOCKET,值设为客户端对象ClientSocket的指针,这样我们需要改为std::map
  在改变储存数据结构后,若想获取客户端socket,则调用iter->first;若想获取客户端对象指针,则调用iter->second;获取已连接客户端数量则还是_clients.size()
  在更换数据结构后,我们通过fdRead.fd_count进行循环,由于linux下fd_set内容与windows下不一致,所以本次要分环境进行检索,代码如下:

#ifdef _WIN32
		for (int n = 0; n < (int)fdRead.fd_count; n++)
		{
     
			auto iter = _clients.find(fdRead.fd_array[n]);
			if (iter != _clients.end())
			{
     
				if (-1 == RecvData(iter->second))
				{
     
					if (_pNetEvent)//主线程中删除客户端 
					{
     
						_pNetEvent->OnNetLeave(iter->second);
					}
					closesocket(iter->first);
					delete iter->second;
					_clients.erase(iter);//移除
					_client_change = true;//客户端退出 需要通知系统重新录入fdset集合 
				}
			}
		}
#else
		std::vector<ClientSocket*> ClientSocket_temp;
		for(auto iter = _clients.begin(); iter != _clients.end(); ++iter)
		{
     
			if (FD_ISSET(iter->first, &fdRead))
			{
     
				if (-1 == RecvData(iter->second))//处理请求 客户端退出的话 
				{
     
					if (_pNetEvent)//主线程中删除客户端 
					{
     
						_pNetEvent->OnNetLeave(iter->second);
					}
					ClientSocket_temp.push_back(iter->second);
					_clients.erase(iter);//移除
					_client_change = true;//客户端退出 需要通知系统重新录入fdset集合 
				}
			}
		}
		for (auto client : ClientSocket_temp)
		{
     
			closesocket(client->GetSockfd());
			_clients.erase(client->GetSockfd());
			delete client;
		}
#endif // _WIN32

  将所有相关位置的代码进行更改后,即可完成 客户端对象储存 数据结构的更改。

2:更改任务队列储存方式

  之前,我是声明了一个抽象任务基类,通过重写基类的DoTask()方法来规定如何执行任务。但是这样利用多态可以执行重写后的任务。但是对于每一个新的任务类型,都需要定义一个新类重写一次DoTask()方法,有点麻烦。所以我使用C++11中新引入的匿名函数,来更改任务队列的储存方式,定义一个匿名函数类型,使任务内容可以更加灵活。

//定义
typedef std::function<void()> CellTask;
//任务数据 
std::list<CellTask>_tasks;

//处理任务
for (auto pTask : _tasks)
{
     
	pTask();	
}

//使用lambda式添加匿名函数
_tasks.push_back([pClient,pHead]() 
{
     
	pClient->SendData(pHead);
	delete pHead;
});

3:加入心跳检测机制

  首先,心跳检测的前提是存在一个计时器,这里我在动态库中新实现了一个计时器类HBtimer(代码如下),通过调用getNowMillSec方法,返回当前时间戳。这样通过一个变量来储存上一次获取的时间戳,从而可以计算两次获取时间戳之间的时间差,从而实现计时功能。

class __declspec(dllexport) HBtimer
{
     
public:
    HBtimer();
    virtual ~HBtimer();
    //获取当前时间戳 (毫秒)
    static time_t getNowMillSec();
};

time_t HBtimer::getNowMillSec()
{
     
    //获取高精度当前时间(毫秒) high_resolution_clock::now();
    //duration_cast是类型转换方法
    return duration_cast<milliseconds>(high_resolution_clock::now().time_since_epoch()).count();
}

  随后我在客户端类中定义一个心跳计时变量,并且声明两个相关方法,实现对心跳计时变量的归零与检测操作。当心跳计时器超过规定的客户端死亡时间后,CheckHeart方法会返回true告知该客户端已死亡。

//客户端死亡时间 20000毫秒
#define CLIENT_HREAT_TIME 20000
//心跳计时器
time_t _dtHeart;
//计时变量归零
void ClientSocket::ResetDtHeart()
{
     
	_dtHeart = 0;
}
//dt为时间差 传入两次检测之间的时间差,检测心跳计时器是否超过心跳检测的阈值
bool ClientSocket::CheckHeart(time_t dt)
{
     
	_dtHeart += dt;
	if (_dtHeart >= CLIENT_HREAT_TIME)
	{
     
		printf("CheakHeart dead:%d,time=%lld\n",_sockfd,_dtHeart);
		return true;
	}
	return false;
}

  接着需要在合适的函数中进行客户端的心跳检测。我在子线程的OnRun方法中,即对客户端进行select操作的方法中,加入CheckTime方法,之后对客户端相关的检测操作均在此方法中进行。在CheckTime中,我们首先要获取两次checktime之间的时间差,随后遍历所有客户端对象,挨个使用CheckHeart方法进行检测是否超时,若发现超时,则主动断开与该客户端之间的连接。

void CellServer::CheckTime()
{
     
	//获取时间差
	time_t nowTime = HBtimer::getNowMillSec();
	time_t dt = nowTime - _oldTime;
	_oldTime = nowTime;
	//遍历所有客户端对象
	for (auto iter = _clients.begin(); iter != _clients.end();)
	{
     
		//检测心跳是否超时
		if (iter->second->CheckHeart(dt) == true)
		{
     
			if (_pNetEvent)//主线程中删除客户端 
			{
     
				_pNetEvent->OnNetLeave(iter->second);
			}
			closesocket(iter->second->GetSockfd());
			delete iter->second;
			_clients.erase(iter++);//移除
			_client_change = true;//客户端退出 需要通知系统重新录入fdset集合 
			continue;
		}
		iter++;
	}
}

  接着是心跳信号,可以在每次收到客户端报文时都对心跳计时变量归零,也可以声明单独的心跳报文,当接收到此报文时,重置心跳计时变量。

//包6 心跳 client to server
struct C2S_Heart : public DataHeader
{
     
	C2S_Heart()//初始化包头 
	{
     
		this->cmd = CMD_C2S_HEART;
		this->date_length = sizeof(C2S_Heart);
	}
};
//包7 心跳 server to client
struct S2C_Heart : public DataHeader
{
     
	S2C_Heart()//初始化包头 
	{
     
		this->cmd = CMD_S2C_HEART;
		this->date_length = sizeof(S2C_Heart);
	}
};

4:加入定时发送缓存消息机制

  之前,我仅进行了客户端消息定量发送功能,即当客户端对象发送缓冲区满后,进行消息的发送。这样当消息发送效率不够高时,很容易造成消息反馈的延迟,于是本次也实现了定时发送的功能。
  上面实现心跳检测时,已经新建了CellServer::CheckTime方法,这个定时发送检测,我们也可以放在这个方法里。思路和心跳检测大同小异,也是在客户端类中定义一个发送计时变量,并且声明两个相关方法,实现对发送计时变量的归零与检测操作。
  当发现需要发送消息时,需要一个方法把客户端对象发送缓冲区内的内容全部发送,并且清空缓冲区(指针归零),随后重置计时变量。该方法为ClientSocket::SendAll

//客户端定时发送时间 200毫秒
#define CLIENT_AUTOMATIC_SEND_TIME 200
//定时发送计时器
time_t _dtSend;
//重置
void ClientSocket::ResetDtSend()
{
     
	_dtSend = 0;
}
//判断
bool ClientSocket::CheckSend(time_t dt)
{
     
	_dtSend += dt;
	if (_dtSend >= CLIENT_AUTOMATIC_SEND_TIME)
	{
     
		//printf("AutomaticSend:%d,time=%lld\n", _sockfd, _dtSend);
		return true;
	}
	return false;
}
//发送缓冲区内全部消息
int ClientSocket::SendAll()
{
     
	int ret = SOCKET_ERROR;
	if (_Len_Send_buf > 0 && SOCKET_ERROR != _sockfd)
	{
     
		//发送 
		ret = send(_sockfd, (const char*)_Msg_Send_buf, _Len_Send_buf, 0);
		//发送后缓冲区归零 
		_Len_Send_buf = 0;
		//重置发送计时器
		ResetDtSend();
		//发送错误 
		if (SOCKET_ERROR == ret)
		{
     
			printf("error 发送失败");
		}
	}
	return ret;
}
//检测
void CellServer::CheckTime()
{
     
	//获取时间差
	time_t nowTime = HBtimer::getNowMillSec();
	time_t dt = nowTime - _oldTime;
	_oldTime = nowTime;
	//遍历所有客户端对象
	for (auto iter = _clients.begin(); iter != _clients.end();)
	{
     
		//检测是否到定时发送消息
		if (iter->second->CheckSend(dt) == true)
		{
     
			iter->second->SendAll();
			iter->second->ResetDtSend();
		}
		iter++;
	}
}

5:将内存池静态库分离

  没什么好说的,简单在vs2019上建一个空项目,随后把项目属性改为静态库,随后把内存池源码搬过去就好。需要注意的一点是静态库分 Debug / Release 版本,记得让源码连接合适的版本。

※ - 项目源码 (github)

提交名:v1.0 定时检测
github项目连接
C++网络编程学习:心跳机制与定时发送数据_第1张图片

  • guguServer为服务端项目
  • guguAlloc为内存池静态库项目
  • guguDll为相关动态库项目
  • debugLib内为debug模式的静态库文件
  • lib内为release模式的静态库和动态库文件

你可能感兴趣的:(网络编程,C/C++,c++,网络,socket,网络编程,C++网络编程)