C++网络编程学习:心跳机制与定时发送数据
Posted 河边小咸鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++网络编程学习:心跳机制与定时发送数据相关的知识,希望对你有一定的参考价值。
网络编程学习记录
- 使用的语言为C/C++
- 源码支持的平台为:Windows(本文中内容使用windows平台下vs2019开发,故本文项目不完全支持linux平台)
C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库) 点我查看之前的代码开发记录
0:本次增改内容
- 更改服务端中,客户端对象储存的方式,由vector改为map。
- 改变任务队列中任务储存方式,由任务基类改为匿名函数。
- 加入心跳检测机制,及时剔除未响应客户端。
- 加入定时发送消息检测机制,及时发送缓冲区内的内容。
- 将内存池静态库分离,使客户端源码也可以引用。
1:更改客户端储存方式
之前,我的服务端程序储存客户端对象ClientSocket
的方式是std::vector<ClientSocket*>
,在select筛选后的fd_set中使用FD_ISSET
函数获取需接收报文的客户端。
但是FD_ISSET
函数是使用for循环进行暴力检索,消耗较大,我们可以改为使用std::map::find
进行检索。这样就需要把储存客户端对象的方式改为std::map
。因为我们需要通过socket进行查找,所以我把std::map
的键设为SOCKET,值设为客户端对象ClientSocket
的指针,这样我们需要改为std::map<SOCKET,ClientSocket*>
。
在改变储存数据结构后,若想获取客户端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项目连接
- guguServer为服务端项目
- guguAlloc为内存池静态库项目
- guguDll为相关动态库项目
- debugLib内为debug模式的静态库文件
- lib内为release模式的静态库和动态库文件
以上是关于C++网络编程学习:心跳机制与定时发送数据的主要内容,如果未能解决你的问题,请参考以下文章