心跳测试的必要性
- 检测非活动连接:如果客户端异常断开(例如,崩溃或网络故障),服务器可能不会立即意识到这一点。心跳测试可以帮助服务器检测出这些“死连接”,从而可以及时清理资源1。
- 保持连接活跃:在某些网络环境中,如果连接在一段时间内没有数据传输,它可能会被网络设备(如防火墙)关闭。通过定期发送心跳包,可以保持连接的活跃状态,防止被意外关闭2。
- 快速响应客户端故障:心跳机制允许服务器更快地检测到客户端的故障,并作出响应,例如重新分配资源或通知其他系统组件3。
- 避免资源浪费:没有心跳测试,服务器可能会保持无效的客户端连接,这会占用宝贵的系统资源,如内存和处理器时间。心跳测试可以帮助避免这种资源浪费4。
CELLTimestamp.cpp文件的变化
class CELLTime //添加获取当前时间的类
{
public:
//获取当前时间戳 (毫秒)
static time_t getNowInMilliSec()
{
return duration_cast<milliseconds>(high_resolution_clock::now().time_since_epoch()).count();
}
};
CELLClient.cpp文件的变化
#ifndef _CellClient_hpp_
#define _CellClient_hpp_
#include"CELL.hpp"
//客户端心跳检测死亡计时时间
#define CLIENT_HREAT_DEAD_TIME 60000
//客户端数据类型
class CellClient
{
public:
void resetDTHeart()
{
_dtHeart = 0;
}
//心跳检测
bool checkHeart(time_t dt)
{
_dtHeart += dt;
if (_dtHeart >= CLIENT_HREAT_DEAD_TIME)
{
printf("checkHeart dead:s=%d,time=%d\n",_sockfd, _dtHeart);
return true;
}
return false;
}
private:
//心跳死亡计时
time_t _dtHeart;
};
#endif // !_CellClient_hpp_
CELLServer.cpp文件的变化
#ifndef _CELL_SERVER_HPP_
#define _CELL_SERVER_HPP_
#include"CELL.hpp"
#include"INetEvent.hpp"
#include"CELLClient.hpp"
#include<vector>
#include<map>
//网络消息接收处理服务类
class CellServer
{
public:
//处理网络消息
//备份客户socket fd_set
fd_set _fdRead_bak;
//客户列表是否有变化
bool _clients_change;
SOCKET _maxSock;
void OnRun()
{
_clients_change = true;
while (isRun())
{
if (!_clientsBuff.empty())
{//从缓冲队列里取出客户数据
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);
//旧的时间戳
_oldTime = CELLTime::getNowInMilliSec();
continue;
}
//伯克利套接字 BSD socket
fd_set fdRead;//描述符(socket) 集合
//清理集合
FD_ZERO(&fdRead);
if (_clients_change)
{
_clients_change = false;
//将描述符(socket)加入集合
_maxSock = _clients.begin()->second->sockfd();
for (auto iter : _clients)
{
FD_SET(iter.second->sockfd(), &fdRead);
if (_maxSock < iter.second->sockfd())
{
_maxSock = iter.second->sockfd();
}
}
memcpy(&_fdRead_bak, &fdRead, sizeof(fd_set));
}
else {
memcpy(&fdRead, &_fdRead_bak, sizeof(fd_set));
}
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
timeval t{ 0,1 };
int ret = select(_maxSock + 1, &fdRead, nullptr, nullptr, &t);
if (ret < 0)
{
printf("select任务结束。\n");
Close();
return;
}
//else if (ret == 0)
//{
// continue;
//}
ReadData(fdRead);
CheckTime();
}
}
//旧的时间戳
time_t _oldTime = CELLTime::getNowInMilliSec();
void CheckTime()
{
//当前时间戳
auto nowTime = CELLTime::getNowInMilliSec();
auto dt = nowTime - _oldTime;
_oldTime = nowTime;
for (auto iter = _clients.begin(); iter != _clients.end(); )
{
if (iter->second->checkHeart(dt))
{
if (_pNetEvent)
_pNetEvent->OnNetLeave(iter->second);
_clients_change = true;
delete iter->second;
auto iterOld = iter;
iter++;
_clients.erase(iterOld);
continue;
}
iter++;
}
}
void ReadData(fd_set& fdRead)
{
#ifdef _WIN32
for (int n = 0; n < 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);
_clients_change = true;
delete iter->second;
closesocket(iter->first);
_clients.erase(iter);
}
}
else {
printf("error. if (iter != _clients.end())\n");
}
}
#else
std::vector<CellClient*> temp;
for (auto iter : _clients)
{
if (FD_ISSET(iter.second->sockfd(), &fdRead))
{
if (-1 == RecvData(iter.second))
{
if (_pNetEvent)
_pNetEvent->OnNetLeave(iter.second);
_clients_change = true;
close(iter->first);
temp.push_back(iter.second);
}
}
}
for (auto pClient : temp)
{
_clients.erase(pClient->sockfd());
delete pClient;
}
#endif
}
private:
};
#endif // !_CELL_SERVER_HPP_
MessageHeader.hpp的变化
#ifndef _MessageHeader_hpp_
#define _MessageHeader_hpp_
enum CMD
{
CMD_C2S_HEART,
CMD_S2C_HEART,
};
struct netmsg_c2s_Heart : public netmsg_DataHeader
{
netmsg_c2s_Heart()
{
dataLength = sizeof(netmsg_c2s_Heart);
cmd = CMD_C2S_HEART;
}
};
struct netmsg_s2c_Heart : public netmsg_DataHeader
{
netmsg_s2c_Heart()
{
dataLength = sizeof(netmsg_s2c_Heart);
cmd = CMD_S2C_HEART;
}
};
#endif // !_MessageHeader_hpp_
server.cpp的变化
#include "EasyTcpServer.hpp"
#include<thread>
bool g_bRun = true;
class MyServer : public EasyTcpServer
{
public:
virtual void OnNetMsg(CellServer* pCellServer, CellClient* pClient, netmsg_DataHeader* header)
{
EasyTcpServer::OnNetMsg(pCellServer, pClient, header);
switch (header->cmd)
{
case CMD_LOGIN:
{
pClient->resetDTHeart();
//send recv
netmsg_Login* login = (netmsg_Login*)header;
//printf("收到客户端<Socket=%d>请求:CMD_LOGIN,数据长度:%d,userName=%s PassWord=%s\n", cSock, login->dataLength, login->userName, login->PassWord);
//忽略判断用户密码是否正确的过程
netmsg_LoginR ret;
pClient->SendData(&ret);
//netmsg_LoginR* ret = new netmsg_LoginR();
//pCellServer->addSendTask(pClient, ret);
}//接收 消息---处理 发送 生产者 数据缓冲区 消费者
break;
case CMD_LOGOUT:
{
netmsg_Logout* logout = (netmsg_Logout*)header;
//printf("收到客户端<Socket=%d>请求:CMD_LOGOUT,数据长度:%d,userName=%s \n", cSock, logout->dataLength, logout->userName);
//忽略判断用户密码是否正确的过程
//netmsg_LogoutR ret;
//SendData(cSock, &ret);
}
break;
case CMD_C2S_HEART:
{
pClient->resetDTHeart();
netmsg_s2c_Heart ret;
pClient->SendData(&ret);
}
default:
{
printf("<socket=%d>收到未定义消息,数据长度:%d\n", pClient->sockfd(), header->dataLength);
//netmsg_DataHeader ret;
//SendData(cSock, &ret);
}
break;
}
}
private:
};