网络编程学习记录
- 使用的语言为C/C++
- 源码支持的平台为:Windows(本文中内容使用windows平台下vs2019开发,故本文项目不完全支持linux平台)
点我查看之前的代码开发记录
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下不一致,所以本次要分环境进行检索,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #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; } } } #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; } } } for (auto client : ClientSocket_temp) { closesocket(client->GetSockfd()); _clients.erase(client->GetSockfd()); delete client; } #endif
|
将所有相关位置的代码进行更改后,即可完成 客户端对象储存 数据结构的更改。
2:更改任务队列储存方式
之前,我是声明了一个抽象任务基类,通过重写基类的DoTask()
方法来规定如何执行任务。但是这样利用多态可以执行重写后的任务。但是对于每一个新的任务类型,都需要定义一个新类重写一次DoTask()
方法,有点麻烦。所以我使用C++11中新引入的匿名函数,来更改任务队列的储存方式,定义一个匿名函数类型,使任务内容可以更加灵活。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef std::function<void()> CellTask;
std::list<CellTask>_tasks;
for (auto pTask : _tasks) { pTask(); }
_tasks.push_back([pClient,pHead]() { pClient->SendData(pHead); delete pHead; });
|
3:加入心跳检测机制
首先,心跳检测的前提是存在一个计时器,这里我在动态库中新实现了一个计时器类HBtimer
(代码如下),通过调用getNowMillSec
方法,返回当前时间戳。这样通过一个变量来储存上一次获取的时间戳,从而可以计算两次获取时间戳之间的时间差,从而实现计时功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class __declspec(dllexport) HBtimer { public: HBtimer(); virtual ~HBtimer(); static time_t getNowMillSec(); };
time_t HBtimer::getNowMillSec() { return duration_cast<milliseconds>(high_resolution_clock::now().time_since_epoch()).count(); }
|
随后我在客户端类中定义一个心跳计时变量,并且声明两个相关方法,实现对心跳计时变量的归零与检测操作。当心跳计时器超过规定的客户端死亡时间后,CheckHeart
方法会返回true告知该客户端已死亡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #define CLIENT_HREAT_TIME 20000
time_t _dtHeart;
void ClientSocket::ResetDtHeart() { _dtHeart = 0; }
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
方法进行检测是否超时,若发现超时,则主动断开与该客户端之间的连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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; continue; } iter++; } }
|
接着是心跳信号,可以在每次收到客户端报文时都对心跳计时变量归零,也可以声明单独的心跳报文,当接收到此报文时,重置心跳计时变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct C2S_Heart : public DataHeader { C2S_Heart() { this->cmd = CMD_C2S_HEART; this->date_length = sizeof(C2S_Heart); } };
struct S2C_Heart : public DataHeader { S2C_Heart() { this->cmd = CMD_S2C_HEART; this->date_length = sizeof(S2C_Heart); } };
|
4:加入定时发送缓存消息机制
之前,我仅进行了客户端消息定量发送功能,即当客户端对象发送缓冲区满后,进行消息的发送。这样当消息发送效率不够高时,很容易造成消息反馈的延迟,于是本次也实现了定时发送的功能。
上面实现心跳检测时,已经新建了CellServer::CheckTime
方法,这个定时发送检测,我们也可以放在这个方法里。思路和心跳检测大同小异,也是在客户端类中定义一个发送计时变量,并且声明两个相关方法,实现对发送计时变量的归零与检测操作。
当发现需要发送消息时,需要一个方法把客户端对象发送缓冲区内的内容全部发送,并且清空缓冲区(指针归零),随后重置计时变量。该方法为ClientSocket::SendAll
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #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) { 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模式的静态库和动态库文件