提交 fe3aeec1 编写于 作者: X xuchi

again

上级 9face247
@echo off
::::::::::::::::::
::key-val
::字典
::std::map<k,v>
::::::::::::::::::
:: 服务端IP地址
set cmd="strIP=127.0.0.1"
:: 服务端端口
set cmd=%cmd% nPort=4567
:: 工作线程数量
set cmd=%cmd% nThread=1
::每个工作线程,创建多少个客户端
set cmd=%cmd% nClient=1000
::::::数据会先写入发送缓冲区
::::::等待socket可写时才实际发送
:: 每个客户端在nSendSleep(毫秒)时间内
:: 最大可写入nMsg条Login消息
:: 每条消息100字节(Login)
set cmd=%cmd% nMsg=1
set cmd=%cmd% nSendSleep=10
:: 客户端发送缓冲区大小(字节)
set cmd=%cmd% nSendBuffSize=10240
:: 客户端接收缓冲区大小(字节)
set cmd=%cmd% nRecvBuffSize=10240
:: 检查接收到的服务端消息ID是否连续
set cmd=%cmd% -checkMsgID
::::::
::::::
:: 启动程序(客户端程序) 传入参数
EasyClient %cmd%
pause
@echo off
::::::::::::::::::
::key-val
::字典
::std::map<k,v>
::::::::::::::::::
::服务端IP地址
set cmd="strIP=any"
::服务端端口
set cmd=%cmd% nPort=4567
::消息处理线程数量
set cmd=%cmd% nThread=1
::客户端连接上限
set cmd=%cmd% nMaxClient=100064
::客户端发送缓冲区大小(字节)
set cmd=%cmd% nSendBuffSize=20480
::客户端接收缓冲区大小(字节)
set cmd=%cmd% nRecvBuffSize=20480
:: 收到消息后将返回应答消息
set cmd=%cmd% -sendback
::提示发送缓冲区已写满
::当出现sendfull提示时,表示当次消息被丢弃
set cmd=%cmd% -sendfull
::检查接收到的客户端消息ID是否连续
set cmd=%cmd% -checkMsgID
::自定义标志 未使用
set cmd=%cmd% -p
::启动程序 传入参数
EasyServer %cmd%
pause
此差异已折叠。
此差异已折叠。
Info [2023-3-7 23:30:0]Log::setLogPath success,<clientLog.txt,w>
Error [2023-3-7 23:30:0]Config::getStr not find <strIP>
Info [2023-3-7 23:30:0]Config::getStr strIP=192.168.43.133
Error [2023-3-7 23:30:0]Config::getStr not find <nPort>
Info [2023-3-7 23:30:0]Config::getInt nPort=4567
Error [2023-3-7 23:30:0]Config::getStr not find <nThread>
Info [2023-3-7 23:30:0]Config::getInt nThread=1
Error [2023-3-7 23:30:0]Config::getStr not find <nClient>
Info [2023-3-7 23:30:0]Config::getInt nClient=1
Error [2023-3-7 23:30:0]Config::getStr not find <nMsg>
Info [2023-3-7 23:30:0]Config::getInt nMsg=1
Error [2023-3-7 23:30:1]Config::getStr not find <nSendSleep>
Info [2023-3-7 23:30:1]Config::getInt nSendSleep=1000
Error [2023-3-7 23:30:1]Config::getStr not find <nWorkSleep>
Info [2023-3-7 23:30:1]Config::getInt nWorkSleep=1
Error [2023-3-7 23:30:1]Config::getStr not find <nSendBuffSize>
Info [2023-3-7 23:30:1]Config::getInt nSendBuffSize=10240
Error [2023-3-7 23:30:1]Config::getStr not find <nRecvBuffSize>
Info [2023-3-7 23:30:1]Config::getInt nRecvBuffSize=8192
Info [2023-3-7 23:30:1]thread<1>,start
Info [2023-3-7 23:30:1]thread<1>,clients<1>,connect<0>,time<1.004325>,send<0>
#ifndef _CELL_BUFFER_HPP_
#define _CELL_BUFFER_HPP_
#include"CELL.hpp"
//#define CELL_USE_IOCP // bug??? 分析:在linux系统下面时,是不支持Iocp的。
#ifdef CELL_USE_IOCP
#include "Iocp.hpp"
#endif
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Buffer
{
public:
Buffer(int nSize = 8192)
{
_nSize = nSize;
_pBuff = new char[_nSize];
}
~Buffer()
{
if (_pBuff)
{
delete[] _pBuff;
_pBuff = nullptr;
}
}
inline char* data() // 内联编译效率更高
{
return _pBuff;
}
inline int DataLen()
{
return _nLast;
}
inline int BuffSize()
{
return _nSize;
}
bool push(const char* pData, int nLen)
{
////写入大量数据不一定要放到内存中
////也可以存储到数据库或者磁盘等存储器中
//if (_nLast + nLen > _nSize)
//{
// //需要写入的数据大于可用空间
// int n = (_nLast + nLen) - _nSize;
// //拓展BUFF
// if (n < 8192)
// n = 8192;
// char* buff = new char[_nSize+n];
// memcpy(buff, _pBuff, _nLast);
// delete[] _pBuff;
// _pBuff = buff;
//}
if (_nLast + nLen <= _nSize)
{
//将要发送的数据 拷贝到发送缓冲区尾部
memcpy(_pBuff + _nLast, pData, nLen);
//计算数据尾部位置
_nLast += nLen;
if (_nLast == SEND_BUFF_SZIE)
{
++_fullCount;
}
return true;
}
else {
++_fullCount;
}
return false;
}
void pop(int nLen)
{
int n = _nLast - nLen;
if (n > 0)
{
memcpy(_pBuff, _pBuff + nLen, n);
}
_nLast = n;
if (_fullCount > 0)
--_fullCount;
}
int write2socket(SOCKET sockfd)
{
int ret = 0;
//缓冲区有数据
if (_nLast > 0 && INVALID_SOCKET != sockfd)
{
// 发送数据
ret = send(sockfd, _pBuff, _nLast, 0);
if (ret <= 0)
{
CELLLog_PError("write2socket1:sockfd<%d> nSize<%d> nLast<%d> ret<%d>", sockfd, _nSize, _nLast, ret);
return SOCKET_ERROR;
}
if (ret == _nLast)
{// _nLast=2000 实际发送ret=2000
// 数据尾部位置清零 缓冲区全部发送才清零 此时不要字节拷贝,字节拷贝有资源消耗的
_nLast = 0;
}
else {
//_nLast=2000 实际发送ret=1000
//CELLLog_Info("write2socket2:sockfd<%d> nSize<%d> nLast<%d> ret<%d>", sockfd, _nSize, _nLast, ret);
_nLast -= ret;
memcpy(_pBuff, _pBuff + ret, _nLast); // 指针偏移 字节拷贝
}
_fullCount = 0;
}
return ret;
}
int read4socket(SOCKET sockfd)
{
if (_nSize - _nLast > 0)
{
// 接收客户端数据
char* szRecv = _pBuff + _nLast;
int nLen = (int)recv(sockfd, szRecv, _nSize - _nLast, 0);
if (nLen <= 0)
{
CELLLog_PError("read4socket:sockfd<%d> nSize<%d> nLast<%d> nLen<%d>", sockfd, _nSize, _nLast, nLen);
return SOCKET_ERROR;
}
// 消息缓冲区的数据尾部位置后移
_nLast += nLen;
return nLen;
}
return 0;
}
bool hasMsg()
{
//判断消息缓冲区的数据长度大于消息头netmsg_DataHeader长度
if (_nLast >= sizeof(netmsg_DataHeader))
{
//这时就可以知道当前消息的长度
netmsg_DataHeader* header = (netmsg_DataHeader*)_pBuff;
if (header != nullptr) {
//判断消息缓冲区的数据长度大于消息长度
return _nLast >= header->dataLength;
}
}
return false;
}
inline bool needWrite()
{
return _nLast > 0;
}
#ifdef CELL_USE_IOCP
//
IO_DATA_BASE* MakeRecvIoData(SOCKET sockfd)
{
int len = _nSize - _nLast; // 接收缓冲区当前剩余空间字节数 // 内部可以直接访问属性和方法,外部才需要提供方法访问属性
if (len > 0) { // 剩余空间大于0可用校验
_ioData.wsabuff.buf = _pBuff + _nLast; // 可写位置=缓冲区首地址+已经存在数据位置
_ioData.wsabuff.len = len;
_ioData.sockfd = sockfd; // 上层调用者传入参数
return &_ioData; // 对象存在属性就存在,属性并不是局部变量
}
return nullptr;
}
//
IO_DATA_BASE* MakeSendIoData(SOCKET sockfd)
{
if (_nLast > 0) { // 缓冲区存在数据
_ioData.wsabuff.buf = _pBuff; // 缓冲区地址
_ioData.wsabuff.len = _nLast; // 缓冲区实际数据长度
_ioData.sockfd = sockfd; // 上层调用者传入参数
return &_ioData; // 对象存在属性就存在,属性并不是局部变量,注意不能返回局部变量地址
}
return nullptr;
}
//
bool Read4Iocp(int nRecv)
{
int nLen = _nSize - _nLast; // 剩余空间
if (nLen >= nRecv) {
_nLast += nRecv; // 当前接收到新数据长度nRecv, 并进行偏移
return true;
}
CELLLog_PError("Read4Iocp:sockfd<%d> nSize<%d> nLast<%d> nLen<%d> nRecv<%d>", _ioData.sockfd, _nSize, _nLast, nLen, nRecv);
return false;
}
// 发送数据(已经发送完成了)(缓冲区采用队列机制:先进先出)
bool Write2Iocp(int nSend)
{
if (_nLast < nSend)
{
// 总结:错误日志很重要,便于快速定位问题
CELLLog_PError("Write2Iocp:sockfd<%d> nSize<%d> nLast<%d> nSend<%d>"
, _ioData.sockfd, _nSize, _nLast, nSend);
return false;
}
if (_nLast == nSend)
{
// 数据尾部位置清零 缓冲区全部发送才清零 此时不要字节拷贝,字节拷贝有资源消耗的
_nLast = 0;
}
else { // _nLast > nSend
_nLast -= nSend;
// 分析:一段连续的内存做数据缓冲区,采用的是队列机制,进来的数据在队列尾部
// ,发送的数据从队列头部取走,缓冲区剩余数据整体前移。
memcpy(_pBuff, _pBuff + nSend, _nLast); // 指针偏移 字节拷贝
}
_fullCount = 0;
return true;
}
#endif
private:
//第二缓冲区 发送缓冲区
char* _pBuff = nullptr;
//可以用链表或队列来管理缓冲数据块
//list<char*> _pBuffList;
//缓冲区的数据尾部位置,已有数据长度
int _nLast = 0;
//缓冲区总的空间大小,字节长度
int _nSize = 0;
//缓冲区写满次数计数
int _fullCount = 0;
#ifdef CELL_USE_IOCP
IO_DATA_BASE _ioData;
#endif
};
} // namespace io
} // namespace doyou
#endif // !_CELL_BUFFER_HPP_
#ifndef _CELL_STREAM_HPP_
#define _CELL_STREAM_HPP_
#include"Log.hpp"
#include<cstdint>
#include<string>
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//字节流BYTE
class ByteStream
{
public:
ByteStream(char* pData, int nSize, bool bDelete = false)
{
_nSize = nSize;
_pBuff = pData;
_bDelete = bDelete;
}
ByteStream(int nSize = 1024)
{
_nSize = nSize;
_pBuff = new char[_nSize];
_bDelete = true;
}
virtual ~ByteStream()
{
if (_bDelete && _pBuff)
{
delete[] _pBuff;
_pBuff = nullptr;
}
}
public:
char* data()
{
return _pBuff;
}
int length()
{
return _nWritePos;
}
//内联函数
//还能读出n字节的数据吗?
inline bool canRead(int n)
{
return _nSize - _nReadPos >= n;
}
//还能写入n字节的数据吗?
inline bool canWrite(int n)
{
return _nSize - _nWritePos >= n;
}
//已写入位置,添加n字节长度
inline void push(int n)
{
_nWritePos += n;
}
//已读取位置,添加n字节长度
inline void pop(int n)
{
_nReadPos += n;
}
inline void setWritePos(int n)
{
_nWritePos = n;
}
//////Read
template<typename T>
bool Read(T& n, bool bOffset = true)
{
//
//计算要读取数据的字节长度
auto nLen = sizeof(T);
//判断能不能读
if (canRead(nLen))
{
//将要读取的数据 拷贝出来
memcpy(&n, _pBuff + _nReadPos, nLen);
//计算已读数据位置
if (bOffset)
pop(nLen);
return true;
}
//断言assert
//错误日志
CELLLog_Error("error, ByteStream::Read failed.");
return false;
}
template<typename T>
bool onlyRead(T& n)
{
return Read(n, false);
}
template<typename T>
uint32_t ReadArray(T* pArr, uint32_t len)
{
uint32_t len1 = 0;
//读取数组元素个数,但不偏移读取位置
Read(len1, false);
// 判断缓存数组能否放得下 分析相等的情况也可以正确读取数据
if (len1 <= len)
{
//计算数组的字节长度
auto nLen = len1 * sizeof(T);
//判断能不能读出
if (canRead(nLen + sizeof(uint32_t)))
{
//计算已读位置+数组长度所占有空间
pop(sizeof(uint32_t));
//将要读取的数据 拷贝出来
memcpy(pArr, _pBuff + _nReadPos, nLen);
//计算已读数据位置
pop(nLen);
return len1;
}
}
CELLLog_Error("ByteStream::ReadArray failed.");
return 0;
}
//char size_t c# char2 char 1
int8_t ReadInt8(int8_t def = 0)
{
Read(def);
return def;
}
//short
int16_t ReadInt16(int16_t n = 0)
{
Read(n);
return n;
}
//int
int32_t ReadInt32(int32_t n = 0)
{
Read(n);
return n;
}
int64_t ReadInt64(int64_t n = 0)
{
Read(n);
return n;
}
uint8_t ReadUInt8(uint8_t def = 0)
{
Read(def);
return def;
}
//short
uint16_t ReadUInt16(uint16_t n = 0)
{
Read(n);
return n;
}
//int
uint32_t ReadUInt32(uint32_t n = 0)
{
Read(n);
return n;
}
uint64_t ReadUInt64(uint64_t n = 0)
{
Read(n);
return n;
}
float ReadFloat(float n = 0.0f)
{
Read(n);
return n;
}
double ReadDouble(double n = 0.0f)
{
Read(n);
return n;
}
bool ReadString(std::string& str)
{
uint32_t nLen = 0;
Read(nLen, false);
if (nLen > 0)
{
//判断能不能读出
if (canRead(nLen + sizeof(uint32_t)))
{
//计算已读位置+数组长度所占有空间
pop(sizeof(uint32_t));
//将要读取的数据 拷贝出来
str.insert(0, _pBuff + _nReadPos, nLen);
//计算已读数据位置
pop(nLen);
return true;
}
}
return false;
}
//////Write
template<typename T>
bool Write(T n)
{
//计算要写入数据的字节长度
auto nLen = sizeof(T);
//判断能不能写入
if (canWrite(nLen))
{
//将要写入的数据 拷贝到缓冲区尾部
memcpy(_pBuff + _nWritePos, &n, nLen);
//计算已写入数据尾部位置
push(nLen);
return true;
}
CELLLog_Error("ByteStream::Write failed.");
return false;
}
template<typename T>
bool WriteArray(T* pData, uint32_t len)
{
//计算要写入数组的字节长度
auto nLen = sizeof(T)*len;
//判断能不能写入
if (canWrite(nLen + sizeof(uint32_t)))
{
//先写入数组的元素数量
Write(len);
//将要写入的数据 拷贝到缓冲区尾部
memcpy(_pBuff + _nWritePos, pData, nLen);
//计算数据尾部位置
push(nLen);
return true;
}
CELLLog_Error("ByteStream::WriteArray failed.");
return false;
}
//char
bool WriteInt8(int8_t n)
{
return Write(n);
}
//short
bool WriteInt16(int16_t n)
{
return Write(n);
}
//int
bool WriteInt32(int32_t n)
{
return Write(n);
}
bool WriteFloat(float n)
{
return Write(n);
}
bool WriteDouble(double n)
{
return Write(n);
}
protected:
//数据缓冲区
char* _pBuff = nullptr;
//缓冲区总的空间大小,字节长度
int _nSize = 0;
//已写入数据的尾部位置,已写入数据长度
int _nWritePos = 0;
//已读取数据的尾部位置
int _nReadPos = 0;
//_pBuff是外部传入的数据块时是否应该被释放
bool _bDelete = true;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_STREAM_HPP_
// file desc: 项目公共头文件,公共定义
#ifndef _CELL_HPP_
#define _CELL_HPP_
// SOCKET
#ifdef _WIN32
#define FD_SETSIZE 65535 // fd_set集合元素的最大值,集合中实际元素数量不能超过这个最大值
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<windows.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#else
#ifdef __APPLE__
// 分析:通过查看macos系统API头文件中详细说明(这是最权威的说明,一般位API开发者提供的解释),只要定义了下面的宏,就解除了select网络模型对socket数量上限值的限定
#define _DARWIN_UNLIMITED_SELECT // 定义一个宏
#endif // !__APPLE__
#include<unistd.h> //uni std
#include<arpa/inet.h>
#include<string.h>
#include<signal.h>
#include<sys/socket.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
//
#include"MessageHeader.hpp"
#include"TimeStamp.hpp"
#include"Task.hpp"
#include"Log.hpp"
//
#include<stdio.h>
//
#ifndef RECV_BUFF_SZIE
#define RECV_BUFF_SZIE 8192
#define SEND_BUFF_SZIE 10240
#endif // !RECV_BUFF_SZIE
#endif // !_CELL_HPP_
#ifndef _CELLClient_HPP_
#define _CELLClient_HPP_
#include"CELL.hpp"
#include"Buffer.hpp"
#include "NetWork.hpp"
// 客户端心跳检测死亡计时时间(单位:ms)
#define CLIENT_HREAT_DEAD_TIME 120000
//在间隔指定时间后才允许发送
#define CLIENT_SEND_BUFF_TIME 200
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//客户端数据类型
class Client
{
//////////用于调试的成员变量
public:
int id = -1;
//所属serverid
int serverId = -1;
//测试接收发逻辑用
//用于server检测接收到的消息ID是否连续
int nRecvMsgID = 1;
//测试接收发逻辑用
//用于client检测接收到的消息ID是否连续
int nSendMsgID = 1;
///////////////////////////////////
public:
Client(SOCKET sockfd = INVALID_SOCKET, int sendSize = SEND_BUFF_SZIE, int recvSize = RECV_BUFF_SZIE) :
_sendBuff(sendSize),
_recvBuff(recvSize)
{
static int n = 1;
id = n++;
_sockfd = sockfd;
resetDTHeart();
resetDTSend();
}
~Client() // 析构函数:在对象生命周期结束时自动调用
{
CELLLog_Info("~Client[sId=%d id=%d socket=%d]", serverId, id, (int)_sockfd);
Destory();
}
// 关闭客户端socket资源
void Destory() // 可主动调用了
{
if (INVALID_SOCKET != _sockfd) // 将常量写在表达式左边是一种推荐的安全写法
{
CELLLog_Info("Client::Destory[sId=%d id=%d socket=%d]", serverId, id, (int)_sockfd);
NetWork::destroy_socket(_sockfd); // 静态方法调用
_sockfd = INVALID_SOCKET;
}
}
SOCKET sockfd()
{
return _sockfd;
}
int RecvData()
{
return _recvBuff.read4socket(_sockfd);
}
bool hasMsg()
{
return _recvBuff.hasMsg();
}
netmsg_DataHeader* front_msg()
{
return (netmsg_DataHeader*)_recvBuff.data();
}
void pop_front_msg()
{
if (hasMsg())
_recvBuff.pop(front_msg()->dataLength);
}
bool needWrite()
{
return _sendBuff.needWrite();
}
//立即将发送缓冲区的数据发送给客户端
int SendDataReal()
{
resetDTSend();
return _sendBuff.write2socket(_sockfd);
}
//缓冲区的控制根据业务需求的差异而调整
//发送数据
int SendData(netmsg_DataHeader* header)
{
return SendData((const char*)header, header->dataLength);
}
int SendData(const char* pData, int len)
{
if (_sendBuff.push(pData, len))
{
return len;
}
return SOCKET_ERROR;
}
void resetDTHeart()
{
_dtHeart = 0;
}
void resetDTSend()
{
_dtSend = 0;
}
//心跳检测
bool checkHeart(time_t dt)
{
_dtHeart += dt;
if (_dtHeart >= CLIENT_HREAT_DEAD_TIME)
{
CELLLog_Info("checkHeart dead:s=%d,time=%ld", _sockfd, _dtHeart);
return true;
}
return false;
}
//定时发送消息检测
bool checkSend(time_t dt)
{
_dtSend += dt;
if (_dtSend >= CLIENT_SEND_BUFF_TIME)
{
//CELLLog_Info("checkSend:s=%d,time=%d", _sockfd, _dtSend);
//立即将发送缓冲区的数据发送出去
SendDataReal();
//重置发送计时
resetDTSend();
return true;
}
return false;
}
// Iocp网络模型特有代码块
#ifdef CELL_USE_IOCP
// 分析:使用Iocp比使用其它网络模型要多两个IO_DATA_BASE部分,方法有两个
// 方法1:采用宏区分,当前正在使用
// 方法2:采用继承Client,在其子类中具体实现
IO_DATA_BASE* MakeRecvIoData() // 投递接收任务准备
{
// 分析:接收任务投递和接收任务完成是一个完整过程(一一对应),不能一次性进行多次接收任务准备
if (_isPostRecv) { // 已经投递过
return nullptr;
}
_isPostRecv = true;
return _recvBuff.MakeRecvIoData(_sockfd);
}
// Iocp接收网络数据
void Recv4Iocp(int nRecv) // 接收数据事件已完成
{
if (!_isPostRecv) { // 逻辑出现错误就日志记录一下
CELLLog_Error("Recv4Iocp _isPostRecv is false.");
}
_isPostRecv = false;
_recvBuff.Read4Iocp(nRecv);
}
IO_DATA_BASE* MakeSendIoData()
{
if (_isPostSend) {
return nullptr; // 避免数据发送任务多次投递
}
_isPostSend = true;
return _sendBuff.MakeSendIoData(_sockfd);
}
// Iocp发送网络数据(已经发送完成)
void Send2Iocp(int nSend)
{
if (!_isPostSend) {
CELLLog_Error("Send2Iocp _isPostSend is false.");
}
_isPostSend = false;
_sendBuff.Write2Iocp(nSend);
}
// 是否投递过接收网络消息或发送网络消息任务
bool IsPostIoAction()
{
return (_isPostRecv || _isPostSend); // 逻辑或:只要有一个及其以上为真,则为真
}
#endif // CELL_USE_IOCP
private:
// socket fd_set file desc set
SOCKET _sockfd;
//第二缓冲区 接收消息缓冲区
Buffer _recvBuff; // 推荐对象组合,优于继承
//发送缓冲区
Buffer _sendBuff;
//心跳死亡计时
time_t _dtHeart;
//上次发送消息数据的时间
time_t _dtSend;
//发送缓冲区遇到写满情况计数
int _sendBuffFullCount = 0;
#ifdef CELL_USE_IOCP
bool _isPostRecv = false; // Iocp是否投递接收数据任务, 也可以采用Int计数,记录投递次数
bool _isPostSend = false; // Iocp是否投递发送数据任务
#endif
};
} // namespace io
} // namespace doyou
#endif // !_CELLClient_HPP_
\ No newline at end of file
#ifndef _CELL_CONFIG_HPP_
#define _CELL_CONFIG_HPP_
/*
专门用于读取配置数据
目前我们的配置参数主要来源于main函数的args传入
*/
#include<string>
#include<map>
#include"Log.hpp" // 分析:在celllog.hpp里面不能用cellconfig.hpp, 存在循环引用问题(这也是将声明和定义写在同一文件的弊端),会出现重定义问题。如果将声明和定义份文件写,就可以互相引用了,因为可以重复声明。
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Config
{
private:
Config()
{
}
~Config()
{
}
public:
static Config& Instance()
{
static Config obj;
return obj;
}
void Init(int argc, char* args[])
{
_exePath = args[0];
for (int n = 1; n < argc; n++)
{
madeCmd(args[n]);
}
}
void madeCmd(char* cmd)
{
//cmd值:strIP=127.0.0.1
char* val = strchr(cmd, '=');
if (val)
{ //val值:=127.0.0.1
*val = '\0';
//cmd值:strIP\0
//val值:\0127.0.0.1
val++;
//val值:127.0.0.1
_kv[cmd] = val;
CELLLog_Debug("madeCmd k<%s> v<%s>", cmd, val);
}
else {
_kv[cmd] = "";
CELLLog_Debug("madeCmd k<%s>", cmd);
}
}
const char* getStr(const char* argName, const char* def)
{
auto itr = _kv.find(argName);
if (itr == _kv.end())
{
CELLLog_Error("Config::getStr not find <%s>", argName);
}
else {
def = itr->second.c_str();
}
CELLLog_Info("Config::getStr %s=%s", argName, def);
return def;
}
int getInt(const char* argName, int def)
{
auto itr = _kv.find(argName);
if (itr == _kv.end())
{
CELLLog_Error("Config::getStr not find <%s>", argName);
}
else {
def = atoi(itr->second.c_str());
}
CELLLog_Info("Config::getInt %s=%d", argName, def);
return def;
}
bool hasKey(const char* key)
{
auto itr = _kv.find(key);
return itr != _kv.end();
}
private:
//当前程序的路径
std::string _exePath;
//存储传入的key-val型数据
std::map<std::string, std::string> _kv;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_CONFIG_HPP_
// 文件描述:封装epoll网络通信模型机制类
#ifndef _CELL_EPOLL_HPP_
#define _CELL_EPOLL_HPP_
#if __linux__ // only linux exist this define
#include "CELL.hpp"
#include "Client.hpp"
#include "Log.hpp"
#include <sys/epoll.h> // epoll net model
#define EPOLL_ERROR (-1)
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Epoll {
public:
Epoll()
{
}
~Epoll() // avoid forget call destory function
{
Destory();
}
// 创建epoll模型
int Create(int nMaxEvents)
{
if (_epfd > 0) {
// record warnning log
Destory();
}
// linux2.6.8 later, nMaxEvents no use
// epoll dyn mana, lilun max according to filemax
// [cat /proc/sys/fs/file-max] cmd to get
// epoll model handle or file desc
// ulimit -n // linux os limit
_epfd = epoll_create(nMaxEvents);
if (EPOLL_ERROR == _epfd) {
//strerror(errno); // linux err display info
//perror("epoll_create:"); // print str info : input string and error info together
CELLLog_PError("epoll_create::errno<%d>,errmsg<%s>", errno, strerror(errno));
return _epfd;
}
// arr ptr -> arr
_pEvents = new epoll_event[nMaxEvents];
_nMaxEvents = nMaxEvents;
return _epfd;
}
// 释放资源
void Destory()
{
if (_epfd > 0) {
NetWork::destroy_socket(_epfd); // close epoll model
_epfd = -1;
}
if (_pEvents != nullptr) {
delete[] _pEvents; // new/delete match
_pEvents = nullptr; //safety
}
}
// add socket to epoll model
int Ctl(int op, SOCKET sockfd, uint32_t events)
{
epoll_event ev;
// event type
ev.events = events; // recv or connect
// event relation to socket objs
ev.data.fd = sockfd; // add socket
// reg socket fd to epoll obj
// spec care events
// return 0-success <0-failure
// add socket to epoll model
int ret = epoll_ctl(_epfd, op, sockfd, &ev);
if (EPOLL_ERROR == ret) {
//perror("epoll_ctl:"); // linux系统自带的错误信息输出到终端方式
CELLLog_PError("epoll_ctl_001");
}
return ret;
}
// add socket to epoll model
int Ctl(int op, Client* pClient, uint32_t events)
{
epoll_event ev;
// event type
ev.events = events; // recv or connect
// event relation to userdata objs
// 利用联合体存储客户端指针,蕴含更多信息,用户自定义指针,使用的时候会原样返回
ev.data.ptr = pClient;
// reg socket fd to epoll obj
// spec care events
// return 0-success <0-failure
// add socket to epoll model
int ret = epoll_ctl(_epfd, op, pClient->sockfd(), &ev);
if (EPOLL_ERROR == ret) {
//perror("epoll_ctl:");
CELLLog_PError("epoll_ctl_002");
}
return ret;
}
// epoll model wait msg events
int Wait(int timeOut)
{
// epfd: epoll obj handle
// events: recv net event array
// maxevents: arr size, can recv events size
// timeout: -1-wait when event happen. 0-return right now. 1>0, wait time 1ms by max
int ret = epoll_wait(_epfd, _pEvents, _nMaxEvents, timeOut); // analysis: 1s will circle 1000 times
if (EPOLL_ERROR == ret) { // ret: events number
if (errno == EINTR) { // 如果当前触发了系统中断事件:优先级更高的程序使得当前程序默认终止
CELLLog_Info("epoll_wait EINTR");
return 0;
}
//perror("epoll_wait:"); // err: record log
CELLLog_PError("epoll_wait");
}
return ret;
}
// epoll happn events
epoll_event* Events()
{
return _pEvents;
}
private:
// save net events array
epoll_event* _pEvents = nullptr;
//
int _nMaxEvents = 256;
// epoll model instance
int _epfd = -1;
};
} // namespace io
} // namespace doyou
#endif // __linux__
#endif // _CELL_EPOLL_HPP_
#ifndef _CELL_EPOLL_SERVER_HPP_
#define _CELL_EPOLL_SERVER_HPP_
#if __linux__
#include "Server.hpp" // 服务端客户端套接字(recv/send)逻辑实现父类
#include "Epoll.hpp" // 采用封装的epoll网络通信模型管理服务器中客户端套接字集合
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 网络消息接收(和发送)处理服务类
// 服务端接收和发送网络消息至客户端
// 继承父类并扩展父类:Server
// epoll网络通信模型
class EpollServer : public Server
{
public:
EpollServer() // 在构造函数里面对类属性进行初始化
{
}
~EpollServer()
{
Close(); // 在释放具体实现的子类对象时机,就先关闭onrun线程
}
//
virtual void SetClientNum(int nSocketNum)
{
_ep.Create(nSocketNum); // 创建epoll模型, 每次处理事件上限就是当前server处理的客户端数量
}
// 处理网络事件:具体实现epoll网络通信模型
virtual bool DoNetEvents()
{
for (auto iter : _clients) { // 分析:对可写的检测,可以大大降低系统性能消耗
// 需要写数据(存在写入数据)的客户端端才注册send事件
if (iter.second->needWrite()) {
_ep.Ctl(EPOLL_CTL_MOD, iter.second, EPOLLIN | EPOLLOUT);
}
else {
_ep.Ctl(EPOLL_CTL_MOD, iter.second, EPOLLIN);
}
}
int ret = _ep.Wait(1);
if (ret < 0)
{
CELLLog_Error("EpollServer%d.OnRun.epoll Error exit.", _id);
return false;
}
else if (ret == 0)
{
return true;
}
// 处理epoll模型中已触发事件
auto events = _ep.Events();
for (int i = 0; i < ret; ++i) { // 遍历事件
Client* pClient = (Client*)events[i].data.ptr; // 取出自定义数据
if (pClient != nullptr) {
if (events[i].events & EPOLLIN) { // recv event happen
if (SOCKET_ERROR == RecvData(pClient))
{
RmClient(pClient);
continue; // 已经移除异常的客户端,就不要在继续后面逻辑了,避免出问题
}
}
if (events[i].events & EPOLLOUT) { // send event happen
if (SOCKET_ERROR == pClient->SendDataReal())
{
RmClient(pClient);
}
}
} // if
} // for
return true;
}
// 移除客户端
void RmClient(Client* pClient)
{
auto iter = _clients.find(pClient->sockfd()); // 分析:指针pClient采用delete后在调用,系统会崩溃(访问空指针问题)
if (iter != _clients.end()) {
_clients.erase(iter);
}
OnClientLeave(pClient);
}
// 有新客户端入队列时调用此接口,只关注recv事件
virtual void OnClientJoin(Client* pClient) // 继承父类的虚函数并重写(如果没有子类继承在重写需要可以不要关键字virtual)
{
_ep.Ctl(EPOLL_CTL_ADD, pClient, EPOLLIN);
}
private:
Epoll _ep;
};
} // namespace io
} // namespace doyou
#endif // __linux__
#endif // !_CELL_EPOLL_SERVER_HPP_
// file description: linux系统突破select网络通信模型fd_set连接数量(1024)限制
// 对fd_set进行自定义封装,不采用系统提供API了
// 避免文件(声明和定义在一个文件里面)重复引用
#ifndef _CELL_FDSET_HPP_
#define _CELL_FDSET_HPP_
#include "CELL.hpp"
//#define CELL_MAX_FD 10240
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class FDSet
{
public:
FDSet()
{
}
~FDSet()
{
Destory();
}
// para1-MaxFds 含义(不同操作系统下面含义不同):
// 在Linux系统下表示socket fd的最大值
// 在Windows系统下表示socket fd的数量
void Create(int MaxFds) // 将构造函数里面操作封装为方法更间接
{
int nSocketNum = MaxFds;
#ifdef _WIN32
// arrays.FD_SETSIZE may be defined by the user before including
// this file, but the default here should be >= 64.
if (nSocketNum < 64) { // 入参安全校验
nSocketNum = 64;
}
// windows计算fd_set占据内存单元字节总数(也是数组元素个数)
_nfdSize = sizeof(u_int) + (sizeof(SOCKET)* nSocketNum);
#else
// 用8KB存储65535个sockfd, 计算原理:
// 在linux系统下面一个bit可以存储一个socket, 8k=8192byte=8192*8bit=65536bit
if (nSocketNum < 65535) { // 入参安全校验
nSocketNum = 65535;
}
// linux计算:数组元素个数 = socket比特位需求总数 / 每一个数组元素可以搞定的socket比特位
// 注意:在unix系统衍生系统下面是采用一个bit位来表示一个socket套接字的
_nfdSize = nSocketNum / (8 * sizeof(char)) + 1; // 考虑整除因素,多申请一个字节内存,可以保证有足够空间可以存储你想存储的socket fd
_MAX_SOCK_FD = nSocketNum;
#endif // _WIN32
// 数组指针:本质是指针,这个指针指向了一个数组
// 指针类型强制转化
_pfdset = (fd_set *)new char[_nfdSize];
// 内存初始化0
memset(_pfdset, 0, _nfdSize);
}
//
void Destory()
{
// 堆中实例化对象new, 用完毕后要采用delete进行释放,避免内存泄露(只借不还)
if (_pfdset)
{
delete[] _pfdset; // 释放的是数组指针哟
_pfdset = nullptr; // 避免出现野指针
}
}
// 在fd_set集合添加套接字socket
inline void add(SOCKET s) // 可以保证指针不为空的场景可以不进行指针判空校验(指针在使用前)
{
// windows系统下面直接添加
#ifdef _WIN32
FD_SET(s, _pfdset); // 在构造函数实例化,一般都会实例化成功,所以没有进行指针为空校验
#else
// linux系统下面进行socket数值校验,必须小于给定的最大值
if (s < _MAX_SOCK_FD)
{
FD_SET(s, _pfdset);
}
else{
CELLLog_Error("FDSet::add sock<%d>, _MAX_SOCK_FD<%d>", (int)s, _MAX_SOCK_FD);
}
#endif // _WIN32
}
// 在fd_set集合删除套接字socket
inline void del(SOCKET s)
{
FD_CLR(s, _pfdset);
}
// 在fd_set集合中清空所有的套接字socket
inline void zero()
{
#ifdef _WIN32
// windows下面直接将数组元素个数属性置零,效率比较高
FD_ZERO(_pfdset);
#else
// linux采用原始方式:字节拷贝
memset(_pfdset, 0, _nfdSize);
#endif // _WIN32
}
// fd_set集合是否存在指定套接字socket
inline bool has(SOCKET s)
{
return FD_ISSET(s, _pfdset); // window/linux下面都有宏FD_ISSET,具体实现可能不同
}
// 属性封装
// 内联函数:走内联编译,如果成功后可以减少函数调用带来的系统开销
inline fd_set* fdset()
{
return _pfdset;
}
void copy(FDSet& set) // 引用传递
{
// 分析:字节拷贝要注意目标内存空间是否足够,此处fdRead和fdRead_bak内存空间是完全一样的
memcpy(_pfdset, set.fdset(), set._nfdSize); // 字节拷贝
}
private:
// 动态创建发fd_set, 在堆中实例化,不在栈中实例化,因为栈内存比较小
fd_set * _pfdset = nullptr;
// fd_set所占内存字节总数(单位:字节)
size_t _nfdSize = 0;
//
int _MAX_SOCK_FD = 0;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_FDSET_HPP_
#ifndef _I_NET_EVENT_HPP_
#define _I_NET_EVENT_HPP_
#include"CELL.hpp"
#include"Client.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//自定义
class Server;
//网络事件接口
class INetEvent
{
public:
//纯虚函数
//客户端加入事件
virtual void OnNetJoin(Client* pClient) = 0;
//客户端离开事件
virtual void OnNetLeave(Client* pClient) = 0;
//客户端消息事件
virtual void OnNetMsg(Server* pServer, Client* pClient, netmsg_DataHeader* header) = 0;
//recv事件
virtual void OnNetRecv(Client* pClient) = 0;
private:
};
} // namespace io
} // namespace doyou
#endif // !_I_NET_EVENT_HPP_
// 为了讲解方便将.h文件(声明文件)和.cpp文件(定义文件)写在了同一个文件.hpp里面
// 封装iocp网络模型,以便在高性能服务器中使用
#ifndef _CELL_IOCP_HPP_
#define _CELL_IOCP_HPP_
#ifdef _WIN32 // Iocp是windows系统特有网络通信模型
#define WIN32_LEAN_AND_MEAN//method2:microsoft提供的一个宏解决socket1.X版本和socket2.X版本重定义问题//
#define _WINSOCK_DEPRECATED_WARNINGS//处理函数inet_ntoa过时bug//
#include <windows.h>//method1:后//use windowsAPI//
#include <WinSock2.h>//method1:先//use windowsAPI//
#include <MSWSock.H> // AcceptEx
#include <stdio.h>
#include "Log.hpp" // 导入日志系统头文件
#pragma comment(lib, "ws2_32.lib") // 添加windowsAPI-WSAStartup使用到了windows的一个静态链接库//注意:windows解决可以,为了实现跨平台可在项目属性配置里面配置这个静态库//
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
enum IO_TYPE { // 自定义客户端数据功能标识
ACCEPT = 10,
RECV,
SEND
};
// 数据缓冲区空间大小(单位:字节)
// 分析:Client为每个客户端对象都开辟了一个数据发送缓冲区和数据接收缓冲区,没有必要再开辟新的数据缓冲区
//#define IO_DATA_BUFF_SIZE 1024
struct IO_DATA_BASE { // Iocp操作IO基本数据结构
// 重叠体(规定:必须有,且是作为数据成员的第一个,才能数据对其,Iocp是这样设计的)
OVERLAPPED overLapped;
//
SOCKET sockfd; // 属性没有括号,方法有括号
// 数据缓冲区(每一个客户端都必须有一个自己的数据缓冲区),指向Client已经开辟的数据缓冲区
//char* buffer;
// 实际缓冲区数据长度
//int length;
WSABUF wsabuff; // 采用系统提供结构,代替成员(buffer/length)
// 操作类型
IO_TYPE iotype;
};
// Iocp事件数据
struct IO_EVENT { // 自定义一个结构体数据新类型
union { // 联合体
void* ptr;
SOCKET sockfd;
} data; // data是一个变量
// 分析:结构体成员放置顺序:内存宽度由大到小排列。
// 分析:IO_DATA_BASE结构体第一个属性是OVERLAPPED, 是这样写的必备前提
SOCKET sockfd = INVALID_SOCKET; // 作为完成键,在c++里面,结构体类型数据成员在声明同时可以进行初始化,推荐。
IO_DATA_BASE* pIOData = nullptr; // IO_DATA_BASE LPOVERLAPPED (注意:这样使用前提是重叠体作为这个数据结构IO_DATA_BASE的第一个属性)
DWORD bytesTrans = 0; // lpNumberOfBytesTransferred 接收数据的字节长度
};
class Iocp {
public:
Iocp() // 构造函数
{
}
virtual ~Iocp() // 虚析构函数
{
Destory(); // 析构函数自动调用回收释放资源函数,利用类对象的生命周期
}
// 创建完成端口Iocp(当前用功能1)
bool Create()
{
// 功能1:创建一个IO完成端口。参数传递:para1-INVALID_HANDLE_VALUE, para2-NULL, para3-0, para4-Iocp允许的并发线程数量(为0默认为CPU数量)
// 功能2:将一个设备和一个IO完成端口相关联。参数传递:para1-设备句柄(文件或socket), para2-Iocp句柄,para3-完成键值,para4-0
_completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (NULL == _completionPort) {
CELLLog_PError("Iocp create failed. CreateIoCompletionPort1");
return false;
}
return true;
}
// 销毁对象
void Destory()
{
if (_completionPort != NULL) {
CloseHandle(_completionPort);
_completionPort = NULL; // 销毁之后,指针赋值空,避免野指针场景
}
}
// 关联Iocp(完成端口:_completionPort)与sockfd
bool Reg(SOCKET sockfd)
{
// para3-完成键(completion key),可以传自定义数据,和epoll模型差不多,事件触发后会原样返回给我们
// 分析:存在同学para3传递的是一个指针变量,采用强制转换的数据类型是DWORD,在32bit系统功能正常
// ,在64bit系统功能异常,原因DWORD数据类型在32和64bit系统下面都是4字节,而指针变量在32bit系统占据4字节,在64bit系统占据8字节。
auto ret = CreateIoCompletionPort((HANDLE)sockfd, _completionPort, (ULONG_PTR)sockfd, 0);
if (!ret) {
CELLLog_PError("Reg Iocp failed. CreateIoCompletionPort2.");
return false;
}
return true;
}
// 关联Iocp(完成端口:_completionPort)与自定义数据地址
// 函数重载:在相同作用域下,函数名相同参数列表不同,可构成函数重载
bool Reg(SOCKET sockfd, void* ptr)
{
// para3-完成键(completion key),可以传自定义数据,和epoll模型差不多,事件触发后会原样返回给我们
// 分析:存在同学para3传递的是一个指针变量,采用强制转换的数据类型是DWORD,在32bit系统功能正常
// ,在64bit系统功能异常,原因DWORD数据类型在32和64bit系统下面都是4字节,而指针变量在32bit系统占据4字节,在64bit系统占据8字节。
auto ret = CreateIoCompletionPort((HANDLE)sockfd, _completionPort, (ULONG_PTR)ptr, 0);
if (!ret) {
CELLLog_PError("Reg Iocp CreateIoCompletionPort2 failed.");
return false;
}
return true;
}
// 向Iocp投递接收客户端连接的任务(异步操作)
bool PostAccept(IO_DATA_BASE* pIOData) // 错误返回机制先不用,但是可以先建立起来备用
{
if (_lpfnAcceptEx == nullptr) { // 指针在使用前都应该进行判空安全校验
CELLLog_PError("PostAccept::_lpfnAcceptEx is null.");
return false;
}
pIOData->iotype = IO_TYPE::ACCEPT; // 接收新客户端加入标识//指针变量间接访问对象
pIOData->sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 提前创建好即将加入的新客户端套接字
char buffer[1024] = {}; // 字符数组,并采用初始化器进行初始化,用来存储新加入客户端的IP地址和端口号等信息,也存在存储网络消息的场景等。
DWORD dwBytes = 0;
OVERLAPPED overLapped = {}; // 结构体初始化器,将结构体对象每个属性都置零了
// 注意1:当para4传入大于0的数值时,就需要客户端connect发起连接,并发送一条网络消息send,iocp服务端才会正式的接受到新客户端加入的事件才算完成;para4-0时,客户端只需connect动作。
// 注意2:这是一个非阻塞函数(异步模式),(阻塞函数的话就是同步模式)
// 分析:向Iocp投递任务主体不是sockServer,而是sockClient相关信息
// 分析:使用加载到内存中的AcceptEx会更高效
// 分析:采用函数指针调用函数,在调用前应确保指针不为空
if (FALSE == _lpfnAcceptEx( // 该函数是异步操作 _lpfnAcceptEx-函数指针 AcceptEx-函数名(也是函数地址)
_sockServer, // 服务端套接字
pIOData->sockfd, // 新加入的客户端套接字
pIOData->wsabuff.buf, // 接收数据存储缓冲区
0, // 接收数据缓冲区大小 sizeof(buffer)-(sizeof(sockaddr_in)+16) * 2 0
sizeof(sockaddr_in) + 16, // 本端地址存储在buffer尾部位置,也就是在接受数据位置尾部开始存储这个本地地址
sizeof(sockaddr_in) + 16, // 远端地址存储在buffer尾部位置,也就是在接受数据位置尾部开始存储这个远端地址
NULL, // 实际接收到的数据的字节数据-&dwBytes
&pIOData->overLapped // 重叠体(类似并发概念,并不完全一致)
)) {
int errCode = WSAGetLastError(); // 获取错误码
if (ERROR_IO_PENDING != errCode) { // ERROR_IO_PENDING: 表示函数执行成功了,后台程序处理中。(Overlapped I/O operation is in progress.)
CELLLog_PError("AcceptEx failed."); // 错误则记录日志
return false;
}
}
return true;
}
// 向Iocp投递客户端接收数据的任务(异步操作)
bool PostRecv(IO_DATA_BASE* pIOData)
{
pIOData->iotype = IO_TYPE::RECV; // 接收客户端数据标识
//wsaBuf.len = IO_DATA_BUFF_SIZE; // 缓冲区大小 // 待处理:记得设置缓冲区长度数据成员数值
DWORD flags = 0;
// para1-内存地址
// para2-字节大小
ZeroMemory(&pIOData->overLapped, sizeof(OVERLAPPED)); // 每次开始接收数据时,置零上次接收到的历史数据
// para1-被取数据的套接字对象
// para2-缓冲区可以传递缓冲区数组,此处先用一个
// para3-缓冲区数组元素个数
// para4-缓冲区实际接收数据字节长度,当此函数用作异步操作时(当前就是),执行时会立即返回,可传NULL
// para7-传递回调函数(当前用不着)
if (SOCKET_ERROR == WSARecv(pIOData->sockfd, &pIOData->wsabuff, 1, NULL, &flags, &pIOData->overLapped, NULL)) {
int errCode = WSAGetLastError(); // 获取错误码
if (ERROR_IO_PENDING != errCode) { // ERROR_IO_PENDING: 表示函数已经执行成功,并处于后台程序处理中
if (WSAECONNRESET != errCode) { // 当errCode==WSAECONNRESET时,不输出日志
CELLLog_PError("WSARecv failed.");
}
return false;
}
}
return true;
}
// 向Iocp投递客户端发送数据的任务(异步操作)
bool PostSend(IO_DATA_BASE* pIOData)
{
pIOData->iotype = IO_TYPE::SEND; // 发送客户端数据标识
WSABUF wsaBuf = {};
// 分析:当前是将接收到的数据原样发送出去,原因传递进来的pIOData保存的是刚刚接收到的数据
// 分析:两个指针变量值相同,都指向了系统同一个对象内存空间而
DWORD flags = 0; // 传递的实参和形参的数据类型必须保持一致或兼容
ZeroMemory(&pIOData->overLapped, sizeof(OVERLAPPED)); // 清空重叠体(历史数据或缓冲区数据均可)
// para1-发送数据的套接字对象
// para2-发送缓冲区
// para3-缓冲区数组元素个数
// para4-缓冲区实际发送数据字节长度,当此函数用作异步操作时(当前),执行时会立即返回,此时可传NULL
// para7-传递回调函数(当前用不着)
// 当前为异步操作模式(执行时会立即返回,不阻塞)
if (SOCKET_ERROR == WSASend(pIOData->sockfd, &pIOData->wsabuff, 1, NULL, flags, &pIOData->overLapped, NULL)) {
int errCode = WSAGetLastError(); // 获取错误码
if (ERROR_IO_PENDING != errCode) { // ERROR_IO_PENDING: 表示函数已经执行成功,并处于后台程序处理中
CELLLog_PError("WSASend failed.");
return false;
}
}
return true;
}
// 检测Iocp完成事件
int Wait(IO_EVENT& ioEvent, int timeout = 100) // 引用传递(与指针传递对比是推荐做法)
{
// 每次填充数据前,先清空历史数据
// 分析: 给结构体按属性赋值的初始化方式比采用memset函数更高效些。
ioEvent.bytesTrans = 0;
ioEvent.pIOData = NULL;
ioEvent.data.ptr = NULL;
// 获取完成端口状态(检测完成事件)
// 检查是否有事件发生,和select/epoll_wait类似
// para1-完成端口,para2-接收数据的字节长度,para3-完成键,会将之前传入的自定义数据原样返回
// para4-填写宏(INFINITE)为阻塞模式,一般会设定一个超时时间,单位millisecond(毫秒),(1s=1000ms,1ms=1000us,1us=1000ns)
// para4-可以获取客户端相关数据(分析:AcceptEx函数第八个传入参数,重叠体-作为我们自定义客户端数据结构的第一个元素,二者地址相同的原理)
// para3-获取服务端套接字(AcceptEx函数第一个传入参数)
// para5-Iocp网络模式延时等待参数设定值(单位:ms)
// 分析:获取的是完成事件的一个队列(队列中有一个至多个元素)
if (FALSE == GetQueuedCompletionStatus(_completionPort
, &ioEvent.bytesTrans
, (PULONG_PTR)&ioEvent.data // 联合体同一时刻只能使用属性之一
, (LPOVERLAPPED*)&ioEvent.pIOData
, timeout)) { // 注意:para5-指针强制转换不可少
int errCode = GetLastError(); // 获取当前错误码
if (WAIT_TIMEOUT == errCode) { // 当错误码是[WAIT_TIMEOUT], 是超时没有事件发送,并不是真正意义的错误
return 0; // 0-success, 超时设定内事件未发生,属于正常场景
}
// 问题分析:pIOData指针使用前应该判空,访问空指针系统直接崩溃(错误访问内存地址问题)
if (ERROR_NETNAME_DELETED == errCode) { // 检测到远端客户端主动断开连接事件
// 分析:远端客户端主动断开链接事件可以被检测到,因为客户端已经和Iocp完成端口进行了关联
//CELLLog_Error("close socket[sockClient=%d]\n", ioEvent.pIOData->sockfd);
return 1; // 正确的取到事件(ioEvent数据已经被正确填充)数据,只是对应socket已经远端断开。
}
if (ERROR_CONNECTION_ABORTED == errCode) {
return 1; // 网络事件发生:本地主动关闭与某个客户端的链接
}
if (ERROR_SEM_TIMEOUT == errCode) { // 异常处理机制
return 1; // 服务端一个线程处理多个客户端对象,不能因为一个客户端出现问题,就影响到其它客户端的正常工作了
}
CELLLog_PError("GetQueuedCompletionStatus failed.");
return -1; // 获取Iocp模型完成事件失败,程序需要终止。 -1-标识执行出现异常
}
return 1; // 完成事件数量1
}
// 将AccepEx加载到内存中,调用效率更高(原理:通过GUID搜索函数)
bool LoadAcceptEx(SOCKET ListenSocket) // 采用此方式会更加高效(与直接使用AccepEx函数)
{
// 安全校验,避免重复调用导致错误
if (INVALID_SOCKET != _sockServer) {
CELLLog_PError("LoadAcceptEx, _sockServer != INVALID_SOCKET");
return false;
}
if (NULL != _lpfnAcceptEx) {
CELLLog_PError("LoadAcceptEx, _lpfnAcceptEx != NULL");
return false;
}
GUID GuidAcceptEx = WSAID_ACCEPTEX; // 全球唯一标识(局部变量)
DWORD dwBytes = 0;
// Load the AcceptEx function into memory using WSAIoctl.
// The WSAIoctl function is an extension of the ioctlsocket()
// function that can use overlapped I/O. The function's 3rd
// through 6th parameters are input and output buffers where
// we pass the pointer to our AcceptEx function. This is used
// so that we can call the AcceptEx function directly, rather
// than refer to the Mswsock.lib library.
int iResult = WSAIoctl(ListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx, sizeof(GuidAcceptEx),
&_lpfnAcceptEx, sizeof(_lpfnAcceptEx), // 注册函数指针
&dwBytes, NULL, NULL);
if (iResult == SOCKET_ERROR) {
CELLLog_PError("WSAIoctl failed.");
return false;
}
// 存储服务端socket
_sockServer = ListenSocket;
return true;
}
private:
LPFN_ACCEPTEX _lpfnAcceptEx = NULL; // 函数指针(AcceptEx)(函数地址)(全局变量)(函数指针)
HANDLE _completionPort = NULL; // NULL-c++以前的空指针写法
SOCKET _sockServer = INVALID_SOCKET; // 服务器socket套接字对象
};
} // namespace io
} // namespace doyou
#endif // _WIN32
#endif // _CELL_IOCP_HPP_
\ No newline at end of file
#ifndef _CELL_IOCP_SERVER_HPP_
#define _CELL_IOCP_SERVER_HPP_
#include "Server.hpp" // 服务端客户端套接字(recv/send)逻辑实现父类
#include "Iocp.hpp" // 采用封装的Iocp网络通信模型管理服务器中客户端套接字集合
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 网络消息接收(和发送)处理服务类
// 服务端接收和发送网络消息至客户端
// 继承父类并扩展父类:Server
// Iocp网络通信模型
class IocpServer : public Server
{
public:
IocpServer() // 在构造函数里面对类属性进行初始化
{
_iocp.Create(); // 创建Iocp模型
}
~IocpServer()
{
Close(); // 在释放具体实现的子类对象时机,就先关闭onrun线程
}
// 处理网络事件:具体实现Iocp网络通信模型(OnRun函数改名的)
virtual bool DoNetEvents()
{
// 分析:Iocp每个客户端是一收一发交替进行机制,不能一次性投递多次接收或投递多次发送
// , 必须先投递接收,当接收数据完成在投递发送,
Client* pClient = nullptr;
for (auto iter = _clients.begin(); iter != _clients.end();) { // 分析:对可写的检测,可以大大降低系统性能消耗
pClient = iter->second; // 指针变量间的值拷贝
// 需要写数据(存在写入数据)的客户端端才注册send事件
if (pClient->needWrite()) {
// 存在数据:投递发送数据任务
auto pIoData = pClient->MakeSendIoData();
if (pIoData != nullptr) {
// 分析:发送数据IO_DATA_BASE先传递过去,准备发送数据,发送数据完成后会告诉我们实际发送数据长度
// 准备发送数据, 把发送数据IO_DATA先给它了,并告知缓冲区大小
if (!_iocp.PostSend(pIoData)) {
OnClientLeave(pClient);
iter = _clients.erase(iter); // 删除之后,iter指向下一个元素(相当于执行iter++)
continue;
}
}
// 存在数据: 投递接收数据任务
pIoData = pClient->MakeRecvIoData(); // 同一变量被多次重复使用
if (pIoData != nullptr) { // 指针在使用前先进行判空处理
// 分析:接收数据IO_DATA_BASE传递过去了,准备接收数据,接收完成后会告诉我们实际接收数据的长度
// 准备接收数据, 把接收数据的IO_DATA给它了,并告知缓冲区大小
if (!_iocp.PostRecv(pIoData)) {
OnClientLeave(pClient);
iter = _clients.erase(iter);
continue;
}
}
}
else {
// 不存在数据: 投递接收数据任务
auto pIoData = pClient->MakeRecvIoData();
if (pIoData != nullptr) { // 指针在使用前先进行判空处理
// 分析:接收数据IO_DATA_BASE传递过去了,准备接收数据,接收完成后会告诉我们实际接收数据的长度
// 准备接收数据, 把接收数据的IO_DATA给它了,并告知缓冲区大小
if (!_iocp.PostRecv(pIoData)) {
OnClientLeave(pClient);
iter = _clients.erase(iter);
continue;
}
}
}
iter++; // 迭代器自增
} // for
while (true) { // 循环处理事件队列中事件,一次处理一个事件,直到事件队列元素为空(ret=0), 跳出循环
// 待优化点:每次只处理一个事件,事件处理性能较差,需要优化处理。
int ret = DoIocpNetEvents();
if (ret < 0) {
// 分析:相同错误日志有且仅有一次是比较合适的
return false;
}
else if (ret == 0) {
return true; // break
}
} // while
return true;
}
// 每次只处理一件网络事件(告诉你这件网络事件完成了-Iocp)
// ret = -1, iocp出错
// ret = 0, 没有事件
// ret = 1, 有事件发生
// 函数返回值:网络事件数量
int DoIocpNetEvents()
{
int ret = _iocp.Wait(_ioEvent, 1);
if (ret < 0) // 模型出错
{
CELLLog_Error("IocpServer%d.OnRun.Iocp Error exit.", _id);
return ret;
}
else if (ret == 0) // 事件发生数量为0
{
return ret;
}
// 接收数据完成-completion(接收数据完成)
if (IO_TYPE::RECV == _ioEvent.pIOData->iotype) {
// 客户端socket事件触发
if (_ioEvent.bytesTrans <= 0) { // 接收到的数据长度小于或等于0,说明接收数据发送错误(比如远端客户端主动断开连接场景)
CELLLog_Info("RmClient socket[sockClient=%d], IO_TYPE::RECV bytesTrans=[%d].\n", _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
RmClient(_ioEvent);
return ret; // 服务端程序当前类对象承接多个客户端,一个异常,其它的还需要正常工作
}
// Reg注册的是:Client*
Client* pClient = (Client*)_ioEvent.data.ptr; // 采用的是c风格的指针类型强制转化
if (pClient != nullptr) {
pClient->Recv4Iocp(_ioEvent.bytesTrans); // 步骤2.0: 回填Iocp实际接收到的数据长度
OnNetRecv(pClient); // 接收网络消息recv调用次数计数
}
//CELLLog_Info("IO_TYPE::RECV: [sockClient=%d], [bytesTrans=%d].\n", _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
// 分析:在适当实际再次投递接收数据任务,缓冲区有限,在不处理数据的前提下不停提交接收数据任务,很容易让缓冲区填满,后就不能继续接收数据了,有校验的
}
// 发送数据完成-completion(注意:不是开始,而是完成,完成端口,事件做完了才会告诉你)
else if (IO_TYPE::SEND == _ioEvent.pIOData->iotype)
{
if (_ioEvent.bytesTrans <= 0) { // 发送数据长度小于或等于0,说明发送数据错误, 比如远端客户端主动断开连接场景
CELLLog_Info("RmClient socket[sockClient=%d], IO_TYPE::SEND bytesTrans=[%d]."
, _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
RmClient(_ioEvent);
return ret;
}
// Reg注册的数据类型是:Client*
Client* pClient = (Client*)_ioEvent.data.ptr;
if (pClient != nullptr) {
pClient->Send2Iocp(_ioEvent.bytesTrans); // 回填Iocp实际发送的数据长度
}
//CELLLog_Info("IO_TYPE::SEND: [sockClient=%d], [bytesTrans=%d]."
//, _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
}
else {
CELLLog_Warring("undefine io_type.");
}
return ret;
}
// 移除客户端1
void RmClient(Client* pClient)
{
auto iter = _clients.find(pClient->sockfd()); // 分析:指针pClient采用delete后在调用,系统会崩溃(访问空指针问题)
if (iter != _clients.end()) {
_clients.erase(iter);
}
OnClientLeave(pClient);
}
// 移除客户端2
void RmClient(IO_EVENT& ioEvent) // 函数重载 引用传递(节省系统资源)
{
// Reg注册的是:Client*
Client* pClient = (Client*)_ioEvent.data.ptr; // bug01: _ioEvent
if (pClient != nullptr) {
// 总结:指针变量间接指向的对象已经释放delete了,再用指针调用系统会直接崩溃(指针指向的对象已经释放后用指针继续访问 或访问空指针都会导致系统直接崩溃)
RmClient(pClient); // 函数重载(函数名相同,参数列表不同,不包括函数返回值)
}
}
// 有新客户端入队列时调用此接口,只关注recv事件
virtual void OnClientJoin(Client* pClient) // 继承父类的虚函数并重写(如果没有子类继承在重写需要可以不要关键字virtual)
{
_iocp.Reg(pClient->sockfd(), pClient); // (void*)类型可以被传入任意类型的指针变量
// 分析:一个客户端要同时有两个IO_DATA_BASE以兼容数据接收和数据发送
auto pIoData = pClient->MakeRecvIoData();
if (pIoData != nullptr) { // 指针在使用前都要进行判空处理
// 分析:接收数据IO_DATA_BASE传递过去了,准备接收数据,接收完成后会告诉我们实际接收数据的长度
_iocp.PostRecv(pIoData); // (客户端)步骤1.0:准备接收数据, 把接收数据的IO_DATA给它了,并告知缓冲区大小
}
}
private:
Iocp _iocp;
IO_EVENT _ioEvent; // 类属性(与之对应有类方法) // 分析:编译器比较旧不支持类的属性在声明时候进行初始化操作
};
} // namespace io
} // namespace doyou
#endif // !_CELL_IOCP_SERVER_HPP_
#ifndef _CELL_LOG_HPP_
#define _CELL_LOG_HPP_
//#include"CELL.hpp"
#include"Task.hpp"
#include<ctime>
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Log
{
//Info 普通信息
//Debug 调试信息,只在debug模式起作用
//Warring 警告信息
//Error 错误信息
#ifdef _DEBUG
#ifndef CELLLog_Debug
#define CELLLog_Debug(...) Log::Debug(__VA_ARGS__)
#endif
#else
#ifndef CELLLog_Debug
#define CELLLog_Debug(...)
#endif
#endif // _DEBUG
// 总结:将日志方法都定义为宏的方式,在不需要日志的时候很方便处理,将宏定义为空的就可以了
#define CELLLog_Info(...) Log::Info(__VA_ARGS__)
#define CELLLog_Warring(...) Log::Warring(__VA_ARGS__)
#define CELLLog_Error(...) Log::Error(__VA_ARGS__)
#define CELLLog_PError(...) Log::PError(__VA_ARGS__)
private:
Log()
{
_taskServer.Start();
}
~Log()
{
_taskServer.Close();
if (_logFile)
{
Info("Log fclose(_logFile)");
fclose(_logFile);
_logFile = nullptr;
}
}
public:
static Log& Instance()
{
static Log sLog;
return sLog;
}
void setLogPath(const char* logName, const char* mode, bool hasDate)
{
if (_logFile)
{
Info("Log::setLogPath _logFile != nullptr");
fclose(_logFile);
_logFile = nullptr;
}
//
static char logPath[256] = {};
//
if (hasDate)
{
auto t = system_clock::now();
auto tNow = system_clock::to_time_t(t);
std::tm* now = std::localtime(&tNow);
sprintf(logPath, "%s[%d-%d-%d_%d-%d-%d].txt", logName, now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec);
}
else {
sprintf(logPath, "%s.txt", logName);
}
//
_logFile = fopen(logPath, mode);
if (_logFile)
{
Info("Log::setLogPath success,<%s,%s>", logPath, mode);
}
else {
Info("Log::setLogPath failed,<%s,%s>", logPath, mode);
}
}
// 总结:普通error打印普通的错误信息,perror打印普通错误信息并且获取系统API出错时机产生的错误码及错误原因。
static void PError(const char* pStr)
{
PError("%s", pStr); // 模板函数(泛型编程)
}
template<typename ...Args> // 函数模板
static void PError(const char* pformat, Args ... args)
{
#ifdef _WIN32 // 分析:如果windows和linux差异教大,总是采用宏来区分,是很坑的。所以,独做两套系统也很好。
auto errCode = GetLastError(); // 在网络消息收发线程中获取错误码
Instance()._taskServer.addTask([=]() { // 在独立的日志任务线程中获取字符串信息
// 采用静态局部变量,生命周期与进程周期保持一致
static char text[256] = {}; // 栈空间申请一个固定数组//char* text = nullptr; // 收发线程中执行:申请text内存
FormatMessageA(
FORMAT_MESSAGE_FROM_SYSTEM, // 采用系统提供,并主动申请内存(无需了)(|FORMAT_MESSAGE_ALLOCATE_BUFFER)
NULL,
errCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 中文
(LPSTR)&text, // 双重指针
256,
NULL
); // ascii编码格式方式
EchoReal(true, "PError ", pformat, args...);
// 不换行,处理页面出现空行不规范场景
EchoReal(false, "PError ", "errno:%d, errmsg: %s", errCode, text); // 实时打印日志
/*
// 分析:当前函数是在一个独立的线程中运行,所以,需要将释放text所申请内存的操作当作一个task任务
// 问题(目前还没有理解?):PError是在task线程队列中执行,所以,由其申请的资源的释放也应该添加到task任务队列中。
Instance()._taskServer.addTask([=]() { // 将任务队列中添加一个新任务(采用匿名函数进行传参)
LocalFree(text); // 日志线程中执行:释放text内存
});*/
});
#else // linux系统
// 分析:多线程调用场景,尽量只获取一次,连续获取两次存在中间被修改的可能性
auto errCode = errno;
Instance()._taskServer.addTask([=]() { // 在独立的日志任务线程中获取字符串信息
EchoReal(true, "PError ", pformat, args...); // 提示信息
EchoReal(false, "PError ", "errno<%d>, errmsg<%s>", errCode, strerror(errCode)); // linux:出现错误时机打印错误码和由错误码转化的错误信息
});
#endif
}
static void Error(const char* pStr)
{
Error("%s", pStr);
}
template<typename ...Args>
static void Error(const char* pformat, Args ... args)
{
Echo("Error ", pformat, args...);
}
static void Warring(const char* pStr)
{
Warring("%s", pStr);
}
template<typename ...Args>
static void Warring(const char* pformat, Args ... args)
{
Echo("Warring ", pformat, args...);
}
static void Debug(const char* pStr)
{
Debug("%s", pStr);
}
template<typename ...Args>
static void Debug(const char* pformat, Args ... args)
{
Echo("Debug ", pformat, args...);
}
static void Info(const char* pStr)
{
Info("%s", pStr);
}
template<typename ...Args>
static void Info(const char* pformat, Args ... args)
{
Echo("Info ", pformat, args...);
}
// 添加到任务线程的执行队列中执行打印日志(丢在了一个独立线程执行,没有在网络消息收发线程中处理,提高网络消息收发能力)
template<typename ...Args>
static void Echo(const char* type, const char* pformat, Args ... args)
{
Log* pLog = &Instance();
pLog->_taskServer.addTask([=]() {
EchoReal(true, type, pformat, args...);
});
}
// 实时执行打印日志 增强功能:para2可以传空指针就不打印type信息的场景
template<typename ...Args>
static void EchoReal(bool br, const char* type, const char* pformat, Args ... args)
{
Log* pLog = &Instance();
if (pLog->_logFile)
{
auto t = system_clock::now();
auto tNow = system_clock::to_time_t(t);
//fprintf(pLog->_logFile, "%s", ctime(&tNow));
std::tm* now = std::localtime(&tNow);
if (type != nullptr) {
fprintf(pLog->_logFile, "%s", type);
}
fprintf(pLog->_logFile, "[%d-%d-%d %d:%d:%d]", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec);
fprintf(pLog->_logFile, pformat, args...);
if (br) { // true-换行
fprintf(pLog->_logFile, "%s", "\n");
}
fflush(pLog->_logFile);
}
if (type != nullptr) {
printf("%s", type);
}
printf(pformat, args...);
if (br) {
printf("%s", "\n");
}
}
private:
FILE* _logFile = nullptr;
TaskServer _taskServer; // 打印日志比较耗时,所以作为了一个独立的线程执行
};
} // namespace io
} // namespace doyou
#endif // !_CELL_LOG_HPP_
#ifndef _MessageHeader_hpp_
#define _MessageHeader_hpp_
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
enum CMD
{
CMD_LOGIN = 10,
CMD_LOGIN_RESULT,
CMD_LOGOUT,
CMD_LOGOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_C2S_HEART,
CMD_S2C_HEART,
CMD_ERROR
};
struct netmsg_DataHeader
{
netmsg_DataHeader()
{
dataLength = sizeof(netmsg_DataHeader);
cmd = CMD_ERROR;
}
unsigned short dataLength;
unsigned short cmd;
};
//DataPackage
struct netmsg_Login : public netmsg_DataHeader
{
netmsg_Login()
{
dataLength = sizeof(netmsg_Login);
cmd = CMD_LOGIN;
}
char userName[32];
char PassWord[32];
char data[28];
int msgID;
};
struct netmsg_LoginR : public netmsg_DataHeader
{
netmsg_LoginR()
{
dataLength = sizeof(netmsg_LoginR);
cmd = CMD_LOGIN_RESULT;
result = 0;
}
int result;
char data[88];
int msgID;
};
struct netmsg_Logout : public netmsg_DataHeader
{
netmsg_Logout()
{
dataLength = sizeof(netmsg_Logout);
cmd = CMD_LOGOUT;
}
char userName[32];
};
struct netmsg_LogoutR : public netmsg_DataHeader
{
netmsg_LogoutR()
{
dataLength = sizeof(netmsg_LogoutR);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
struct netmsg_NewUserJoin : public netmsg_DataHeader
{
netmsg_NewUserJoin()
{
dataLength = sizeof(netmsg_NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
scok = 0;
}
int scok;
};
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;
}
};
} // namespace io
} // namespace doyou
#endif // !_MessageHeader_hpp_
\ No newline at end of file
#ifndef _CELL_MSG_STREAM_HPP_
#define _CELL_MSG_STREAM_HPP_
#include"MessageHeader.hpp"
#include"ByteStream.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//消息数据字节流
class ReadByteStream :public ByteStream
{
public:
ReadByteStream(netmsg_DataHeader* header)
:ReadByteStream((char*)header, header->dataLength)
{
}
ReadByteStream(char* pData, int nSize, bool bDelete = false)
:ByteStream(pData, nSize, bDelete)
{
push(nSize);
////预先读取消息长度
//ReadInt16();
////预先读取消息命令
//getNetCmd();
}
uint16_t getNetCmd()
{
uint16_t cmd = CMD_ERROR;
Read<uint16_t>(cmd);
return cmd;
}
//////Read
template<typename T>
bool Read(T& n, bool bOffset = true)
{
//
//计算要读取数据的字节长度
auto nLen = sizeof(T);
//判断能不能读
if (canRead(nLen))
{
//将要读取的数据 拷贝出来
memcpy(&n, _pBuff + _nReadPos, nLen);
//计算已读数据位置
if (bOffset)
pop(nLen);
return true;
}
//断言assert
//错误日志
CELLLog_Error("error, ByteStream::Read failed.");
return false;
}
template<typename T>
bool onlyRead(T& n)
{
return Read(n, false);
}
template<typename T>
uint32_t ReadArray(T* pArr, uint32_t len)
{
uint32_t len1 = 0;
//读取数组元素个数,但不偏移读取位置
Read(len1, false);
// 判断缓存数组能否放得下 分析相等的情况也可以正确读取数据
if (len1 <= len)
{
//计算数组的字节长度
auto nLen = len1 * sizeof(T);
//判断能不能读出
if (canRead(nLen + sizeof(uint32_t)))
{
//计算已读位置+数组长度所占有空间
pop(sizeof(uint32_t));
//将要读取的数据 拷贝出来
memcpy(pArr, _pBuff + _nReadPos, nLen);
//计算已读数据位置
pop(nLen);
return len1;
}
}
CELLLog_Error("ByteStream::ReadArray failed.");
return 0;
}
//char size_t c# char2 char 1
int8_t ReadInt8(int8_t def = 0)
{
Read(def);
return def;
}
//short
int16_t ReadInt16(int16_t n = 0)
{
Read(n);
return n;
}
//int
int32_t ReadInt32(int32_t n = 0)
{
Read(n);
return n;
}
int64_t ReadInt64(int64_t n = 0)
{
Read(n);
return n;
}
uint8_t ReadUInt8(uint8_t def = 0)
{
Read(def);
return def;
}
//short
uint16_t ReadUInt16(uint16_t n = 0)
{
Read(n);
return n;
}
//int
uint32_t ReadUInt32(uint32_t n = 0)
{
Read(n);
return n;
}
uint64_t ReadUInt64(uint64_t n = 0)
{
Read(n);
return n;
}
float ReadFloat(float n = 0.0f)
{
Read(n);
return n;
}
double ReadDouble(double n = 0.0f)
{
Read(n);
return n;
}
bool ReadString(std::string& str)
{
uint32_t nLen = 0;
Read(nLen, false);
if (nLen > 0)
{
//判断能不能读出
if (canRead(nLen + sizeof(uint32_t)))
{
//计算已读位置+数组长度所占有空间
pop(sizeof(uint32_t));
//将要读取的数据 拷贝出来
str.insert(0, _pBuff + _nReadPos, nLen);
//计算已读数据位置
pop(nLen);
return true;
}
}
return false;
}
};
//消息数据字节流
class WriteByteStream :public ByteStream
{
public:
WriteByteStream(char* pData, int nSize, bool bDelete = false)
:ByteStream(pData, nSize, bDelete)
{
//预先占领消息长度所需空间
Write<uint16_t>(0);
}
WriteByteStream(int nSize = 1024)
:ByteStream(nSize)
{
//预先占领消息长度所需空间
Write<uint16_t>(0);
}
void setNetCmd(uint16_t cmd)
{
Write<uint16_t>(cmd);
}
bool WriteString(const char* str, int len)
{
return WriteArray(str, len);
}
bool WriteString(const char* str)
{
return WriteArray(str, strlen(str));
}
bool WriteString(std::string& str)
{
return WriteArray(str.c_str(), str.length());
}
void finsh()
{
int pos = length();
setWritePos(0);
Write<uint16_t>(pos);
setWritePos(pos);
}
//////Write
template<typename T>
bool Write(T n)
{
//计算要写入数据的字节长度
auto nLen = sizeof(T);
//判断能不能写入
if (canWrite(nLen))
{
//将要写入的数据 拷贝到缓冲区尾部
memcpy(_pBuff + _nWritePos, &n, nLen);
//计算已写入数据尾部位置
push(nLen);
return true;
}
CELLLog_Error("ByteStream::Write failed.");
return false;
}
template<typename T>
bool WriteArray(T* pData, uint32_t len)
{
//计算要写入数组的字节长度
auto nLen = sizeof(T)*len;
//判断能不能写入
if (canWrite(nLen + sizeof(uint32_t)))
{
//先写入数组的元素数量
Write(len);
//将要写入的数据 拷贝到缓冲区尾部
memcpy(_pBuff + _nWritePos, pData, nLen);
//计算数据尾部位置
push(nLen);
return true;
}
CELLLog_Error("ByteStream::WriteArray failed.");
return false;
}
//char
bool WriteInt8(int8_t n)
{
return Write(n);
}
//short
bool WriteInt16(int16_t n)
{
return Write(n);
}
//int
bool WriteInt32(int32_t n)
{
return Write(n);
}
bool WriteFloat(float n)
{
return Write(n);
}
bool WriteDouble(double n)
{
return Write(n);
}
};
} // namespace io
} // namespace doyou
#endif // !_CELL_MSG_STREAM_HPP_
#ifndef _CELL_NET_WORK_HPP_
#define _CELL_NET_WORK_HPP_
#include"CELL.hpp"
#include"Log.hpp"
#ifndef _WIN32
#include<fcntl.h>
#include<stdlib.h>
#endif // !_WIN32
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class NetWork
{
private:
NetWork()
{
#ifdef _WIN32
//启动Windows socket 2.x环境
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
#endif
#ifndef _WIN32
//if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
// return (1);
//忽略异常信号,默认情况会导致进程终止
signal(SIGPIPE, SIG_IGN);
#endif
}
~NetWork()
{
#ifdef _WIN32
//清除Windows socket环境
WSACleanup();
#endif
}
public:
static void Init()
{
static NetWork obj;
}
static int make_nonblocking(SOCKET fd)
{
#ifdef _WIN32
{
unsigned long nonblocking = 1;
if (ioctlsocket(fd, FIONBIO, &nonblocking) == SOCKET_ERROR) {
CELLLog_Warring("fcntl(%d, F_GETFL)", (int)fd);
return -1;
}
}
#else
{
int flags;
if ((flags = fcntl(fd, F_GETFL, NULL)) < 0) {
CELLLog_Warring("fcntl(%d, F_GETFL)", fd);
return -1;
}
if (!(flags & O_NONBLOCK)) {
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
CELLLog_Warring("fcntl(%d, F_SETFL)", fd);
return -1;
}
}
}
#endif
return 0;
}
static int make_reuseaddr(SOCKET fd) // 端口重用
{
int flag = 1;
if (SOCKET_ERROR == setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&flag, sizeof(flag))) { // 函数返回值推荐采用宏,代码更清晰
CELLLog_Warring("setsockopt socket<%d> SO_REUSEADDR fail", (int)fd);
return SOCKET_ERROR;
}
return 0;
}
// 关闭socket资源(封装方法)
static int destroy_socket(SOCKET sockfd)
{
int ret = 0;
#ifdef _WIN32
ret = closesocket(sockfd);
#else
ret = close(sockfd);
#endif
if (ret < 0) { // 关闭socket出错
CELLLog_PError("destroy socket<%d> failed.", (int)sockfd); // 记录错误码及错误原因
}
return ret;
}
};
} // namespace io
} // namespace doyou
#endif // !_CELL_NET_WORK_HPP_
#ifndef _CELL_SELECT_SERVER_HPP_
#define _CELL_SELECT_SERVER_HPP_
#include"Server.hpp"
#include <vector> // 重复包含没有关系,因为存在宏校验,尽量避免重复包含相同头文件
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 网络消息接收(和发送)处理服务类
// 服务端接收和发送网络消息至客户端
// 继承父类并扩展父类:Server
// select网络通信模型
class SelectServer : public Server
{
public:
// 在释放子类对象时,就先自己关闭onrun执行线程,就不会出现onrun线程在当前对象析构后还来调用当前类对象的方法了(会直接出错)
~SelectServer()
{
Close(); // 调用之后会关闭onrun函数所在执行线程
}
// 设置客户端数量
virtual void SetClientNum(int nSocketNum)
{
_fdRead.Create(nSocketNum);
_fdWrite.Create(nSocketNum);
_fdRead_bak.Create(nSocketNum);
}
// 处理网络事件:具体实现select网络通信模型
// 重写父类的虚函数(不是函数重载,而是函数重写)
bool DoNetEvents() // select网络通信模型
{
// 计算可读集合
// 分析:备份优化在windows系统下面效果明显,在linux系统下面优化幅度不大,因为linux系统下面采用的高效的位运算,本来消耗系统资源都不多
if (_clients_change)
{
_clients_change = false;
// 清理集合
_fdRead.zero();
// 将描述符(socket)加入集合 存储集合中最大的那个套接字
_maxSock = _clients.begin()->second->sockfd();
for (auto iter : _clients)
{
_fdRead.add(iter.second->sockfd()); // 将所有的客户端socket都加入可写fd_set集合中
if (_maxSock < iter.second->sockfd())
{
_maxSock = iter.second->sockfd();
}
}
// 数据备份
// 分析:对象的属性在外部暴漏有安全风险要尽量避免
_fdRead_bak.copy(_fdRead);
}
else {
_fdRead.copy(_fdRead_bak);
}
// 计算可写集合
bool bNeedWrite = false;
_fdWrite.zero();
for (auto iter : _clients)
{ // 需要写数据的客户端,才加入fd_set检测是否可写
if (iter.second->needWrite())
{
bNeedWrite = true;
_fdWrite.add(iter.second->sockfd());
}
}
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
timeval t{ 0, 1 }; // 结构体初始化方式
int ret = 0;
// 可写入集合校验,在无数据可写的时候可写入集合为空,阻塞会生效,可以节省系统资源开销
if (bNeedWrite)
{
ret = select(_maxSock + 1, _fdRead.fdset(), _fdWrite.fdset(), nullptr, &t);
}
else {
ret = select(_maxSock + 1, _fdRead.fdset(), nullptr, nullptr, &t);
}
if (ret < 0) // 错误一般返回SOCKETERROR == -1
{
if (errno == EINTR) { // 处理可能出现的系统终端导致的程序终止
//CELLLog_PError("SelectServer.OnRun select exit, deal errno == EINTR");
return true; // 忽略系统终端信号[EINTR], 程序继续正常运行
}
CELLLog_PError("SelectServer%d.OnRun.select.", _id);
return false;
}
else if (ret == 0)
{
return true;
}
ReadData();
WriteData();
return true;
}
// 发送网络消息
void WriteData()
{
#ifdef _WIN32
auto pfdset = _fdWrite.fdset();
for (int n = 0; n < pfdset->fd_count; n++)
{
auto iter = _clients.find(pfdset->fd_array[n]);
if (iter != _clients.end())
{
if (SOCKET_ERROR == iter->second->SendDataReal())
{
OnClientLeave(iter->second);
_clients.erase(iter);
}
}
}
#else
for (auto iter = _clients.begin(); iter != _clients.end();)
{
if (iter->second->needWrite() && _fdWrite.has(iter->second->sockfd()))
{
if (SOCKET_ERROR == iter->second->SendDataReal())
{
OnClientLeave(iter->second);
auto iterOld = iter;
iter++;
_clients.erase(iterOld);
continue;
}
}
iter++;
}
#endif
}
// 接收网络消息
void ReadData()
{
#ifdef _WIN32
auto pfdset = _fdRead.fdset(); // 指针变量的拷贝 // recv事件的集合
for (int n = 0; n < pfdset->fd_count; n++)
{
auto iter = _clients.find(pfdset->fd_array[n]);
if (iter != _clients.end())
{
if (SOCKET_ERROR == RecvData(iter->second))
{
OnClientLeave(iter->second);
_clients.erase(iter);
}
}
}
#else
for (auto iter = _clients.begin(); iter != _clients.end();)
{
if (_fdRead.has(iter->second->sockfd()))
{
if (SOCKET_ERROR == RecvData(iter->second))
{
OnClientLeave(iter->second);
auto iterOld = iter;
iter++;
_clients.erase(iterOld); // 删掉异常断开的客户端
continue;
}
}
iter++;
}
#endif
}
private:
// 伯克利套接字 BSD socket
// 描述符(socket) 集合
// 分析:将局部变量优化为属性,可以减少创建和释放对象频次,降低资源开销
FDSet _fdRead; // 可读集合,接收数据请求网络消息
FDSet _fdWrite; // 可写集合,发送数据网络消息
// 备份客户socket fd_set
FDSet _fdRead_bak;
//
SOCKET _maxSock;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_SELECT_SERVER_HPP_
#ifndef _CELL_SEMAPHORE_HPP_
#define _CELL_SEMAPHORE_HPP_
#include<chrono>
#include<thread>
#include<condition_variable>
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//信号量
class Semaphore
{
public:
//阻塞当前线程
void wait()
{
std::unique_lock<std::mutex> lock(_mutex);
if (--_wait < 0)
{
//阻塞等待
_cv.wait(lock, [this]()->bool{
return _wakeup > 0;
});
--_wakeup;
}
}
//唤醒当前线程
void wakeup()
{
std::lock_guard<std::mutex> lock(_mutex);
if (++_wait <= 0)
{
++_wakeup;
_cv.notify_one();
}
}
private:
//改变数据缓冲区时需要加锁
std::mutex _mutex;
//阻塞等待-条件变量
std::condition_variable _cv;
//等待计数
int _wait = 0;
//唤醒计数
int _wakeup = 0;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_SEMAPHORE_HPP_
//虚假唤醒
\ No newline at end of file
#ifndef _CELL_SERVER_HPP_
#define _CELL_SERVER_HPP_
#include"CELL.hpp"
#include"INetEvent.hpp"
#include"Client.hpp"
#include"Semaphore.hpp"
#include"FDSet.hpp" // 导入头文件
#include<vector>
#include<map>
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 网络消息接收(和发送)处理服务类
// 服务端接收和发送网络消息至客户端
class Server
{
public:
Server()
{
_pNetEvent = nullptr; // 推荐采用在属性声明位置一并完成需要的初始化动作
}
virtual ~Server()
{
CELLLog_Info("Server%d.~Server exit begin", _id);
Close();
CELLLog_Info("Server%d.~Server exit end", _id);
}
void SetId(int id)
{
_id = id;
_taskServer.serverId = id;
}
// 设置客户端数量
virtual void SetClientNum(int nSocketNum) // 父类虚方法,由子类继承后,具体加以实现
{
}
void setEventObj(INetEvent* event)
{
_pNetEvent = event;
}
//关闭Socket
void Close()
{
CELLLog_Info("Server%d.Close begin", _id);
_taskServer.Close();
_thread.Close();
CELLLog_Info("Server%d.Close end", _id);
}
//处理网络消息
void OnRun(Thread* pThread)
{
while (pThread->isRun())
{
if (!_clientsBuff.empty())
{// 从缓冲队列里取出客户数据
std::lock_guard<std::mutex> lock(_mutex);
for (auto pClient : _clientsBuff)
{
_clients[pClient->sockfd()] = pClient;
pClient->serverId = _id;
if (_pNetEvent) // 不为空表明已经注册,触发事件
_pNetEvent->OnNetJoin(pClient);
OnClientJoin(pClient);
}
_clientsBuff.clear();
_clients_change = true;
}
//如果没有需要处理的客户端,就跳过
if (_clients.empty())
{
Thread::Sleep(1);
//旧的时间戳
_oldTime = Time::getNowInMilliSec();
continue;
}
CheckTime();
if (!DoNetEvents())
{
pThread->Exit();
break;
}
DoMsg();
}
CELLLog_Info("Server%d.OnRun exit", _id);
}
// 处理网络事件(纯虚函数):具体实现有select网络通信模型、epoll网络通信模型。
// 当前类作为基类
virtual bool DoNetEvents() = 0;
void CheckTime()
{
// 当前时间戳
auto nowTime = Time::getNowInMilliSec();
auto dt = nowTime - _oldTime; // 累计计时
_oldTime = nowTime;
Client* pClient = nullptr;
for (auto iter = _clients.begin(); iter != _clients.end();) // map遍历
{
pClient = iter->second;
// 心跳检测
if (pClient->checkHeart(dt)) // true-客户端心跳超时
{
#ifdef CELL_USE_IOCP // 采用宏来区分使用的具体网络模型
if (pClient->IsPostIoAction()) { // 待处理问题:此场景什么时候delete pClient呢,很可能存在内存泄露问题,bug?
pClient->Destory(); // 存在投递任务事件,后台线程还可能会调用客户端,只能先关闭socket
}
else {
OnClientLeave(pClient); // 不存在投递后台网络事件,可以整体关闭客户端。
}
#else
OnClientLeave(pClient); // 非Iocp网络模型直接移除客户端
#endif // CELL_USE_IOCP
iter = _clients.erase(iter); // 更简单的方法:map通过迭代器删除一个元素后,迭代器会指向被删除元素的下一个元素(相当与执行iter++)
continue;
}
////定时发送检测
//pClient->checkSend(dt);
iter++;
}
}
void OnClientLeave(Client* pClient)
{
if (_pNetEvent)
_pNetEvent->OnNetLeave(pClient);
_clients_change = true;
delete pClient; // 释放系统资源
}
virtual void OnClientJoin(Client* pClient) // 虚方法由子类继承并具体实现(普通虚函数,不用做成纯虚函数)
{
}
//
void OnNetRecv(Client* pClient) // 在父类写一个方法,提供给子类调用
{
if (_pNetEvent != nullptr) {
_pNetEvent->OnNetRecv(pClient);
}
}
void DoMsg()
{
Client* pClient = nullptr;
for (auto itr : _clients)
{
pClient = itr.second;
while (pClient->hasMsg())
{
//处理网络消息
OnNetMsg(pClient, pClient->front_msg());
//移除消息队列(缓冲区)最前的一条数据
pClient->pop_front_msg();
}
}
}
// 接收数据 处理粘包 拆分包
int RecvData(Client* pClient)
{
// 接收客户端数据
int nLen = pClient->RecvData();
// 触发<接收到网络数据>事件
if (_pNetEvent != nullptr) {
_pNetEvent->OnNetRecv(pClient);
}
return nLen;
}
// 响应网络消息
virtual void OnNetMsg(Client* pClient, netmsg_DataHeader* header)
{
if (_pNetEvent != nullptr) { // 指针在使用前推荐先进行判空校验,更安全
_pNetEvent->OnNetMsg(this, pClient, header);
}
}
void addClient(Client* pClient)
{
std::lock_guard<std::mutex> lock(_mutex);
//_mutex.lock();
_clientsBuff.push_back(pClient);
//_mutex.unlock();
}
void Start()
{
_taskServer.Start();
_thread.Start(
//onCreate
nullptr,
//onRun
[this](Thread* pThread) {
OnRun(pThread);
},
//onDestory
[this](Thread* pThread) {
ClearClients();
}
);
}
size_t getClientCount()
{
return _clients.size() + _clientsBuff.size();
}
//void addSendTask(Client* pClient, netmsg_DataHeader* header)
//{
// _taskServer.addTask([pClient, header]() {
// pClient->SendData(header);
// delete header;
// });
//}
private:
void ClearClients()
{
for (auto iter : _clients)
{
delete iter.second;
}
_clients.clear();
for (auto iter : _clientsBuff)
{
delete iter;
}
_clientsBuff.clear();
}
protected:
// 正式客户队列
std::map<SOCKET, Client*> _clients;
private:
//缓冲客户队列
std::vector<Client*> _clientsBuff;
//缓冲队列的锁
std::mutex _mutex;
//网络事件对象
INetEvent* _pNetEvent = nullptr;
//
TaskServer _taskServer;
//旧的时间戳
time_t _oldTime = Time::getNowInMilliSec();
//
Thread _thread;
protected:
//
int _id = -1;
//客户列表是否有变化
bool _clients_change = true;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_SERVER_HPP_
#ifndef _CELL_TASK_H_
#define _CELL_TASK_H_
#include<thread>
#include<mutex>
#include<list>
#include<functional>
#include"Thread.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
//执行任务的服务类型
class TaskServer
{
public:
//所属serverid
int serverId = -1;
private:
typedef std::function<void()> Task;
private:
//任务数据
std::list<Task> _tasks;
//任务数据缓冲区
std::list<Task> _tasksBuf;
//改变数据缓冲区时需要加锁
std::mutex _mutex;
//
Thread _thread;
public:
//添加任务
void addTask(Task task)
{
std::lock_guard<std::mutex> lock(_mutex);
_tasksBuf.push_back(task);
}
//启动工作线程
void Start()
{
_thread.Start(nullptr, [this](Thread* pThread) {
OnRun(pThread);
});
}
void Close()
{
///CELLLog_Info("TaskServer%d.Close begin", serverId);
_thread.Close();
//CELLLog_Info("TaskServer%d.Close end", serverId);
}
protected:
//工作函数
void OnRun(Thread* pThread)
{
while (pThread->isRun())
{
//从缓冲区取出数据
if (!_tasksBuf.empty())
{
std::lock_guard<std::mutex> lock(_mutex);
for (auto pTask : _tasksBuf)
{
_tasks.push_back(pTask);
}
_tasksBuf.clear();
}
//如果没有任务
if (_tasks.empty())
{
Thread::Sleep(1);
continue;
}
//处理任务
for (auto pTask : _tasks)
{
pTask();
}
//清空任务
_tasks.clear();
}
//处理缓冲队列中的任务
for (auto pTask : _tasksBuf)
{
pTask();
}
//CELLLog_Info("TaskServer%d.OnRun exit", serverId);
}
};
} // namespace io
} // namespace doyou
#endif // !_CELL_TASK_H_
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_
#include "CELL.hpp"
#include "NetWork.hpp"
#include "MessageHeader.hpp"
#include "Client.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class TcpClient // 并没有在内部建立一个独立的线程
{
public:
TcpClient()
{
_isConnect = false;
}
virtual ~TcpClient()
{
Close();
}
// 初始化socket(客户端程序)
SOCKET InitSocket(int sendSize = SEND_BUFF_SZIE, int recvSize = RECV_BUFF_SZIE)
{
NetWork::Init();
if (_pClient)
{
CELLLog_Info("warning, initSocket close old socket<%d>...", (int)_pClient->sockfd());
Close();
}
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sock)
{
CELLLog_PError("create socket failed...");
}
else {
// 客户端程序socket套接字(描述符)绑定的端口关闭,立即重用也可能会端口绑定失败,因为系统还没有释放资源,所以,也应该设置端口立即关闭生效属性
NetWork::make_reuseaddr(sock);
//CELLLog_Info("create socket<%d> success...", (int)sock);
_pClient = new Client(sock, sendSize, recvSize);
OnInitSocket(); // 触发内部事件,利用多态机制:当前父类装的那个子类对象就调用那个子类对象重写的父类的虚函数,通知子类socket已经创建。
}
return sock;
}
//连接服务器
int Connect(const char* ip, unsigned short port)
{
if (!_pClient)
{
if (INVALID_SOCKET == InitSocket())
{
return SOCKET_ERROR;
}
}
// 2 连接服务器 connect
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(port);
#ifdef _WIN32
_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
_sin.sin_addr.s_addr = inet_addr(ip);
#endif
//CELLLog_Info("<socket=%d> connecting <%s:%d>...", (int)_pClient->sockfd(), ip, port);
int ret = connect(_pClient->sockfd(), (sockaddr*)&_sin, sizeof(sockaddr_in));
if (SOCKET_ERROR == ret)
{
CELLLog_PError("<socket=%d> connect <%s:%d> failed...", (int)_pClient->sockfd(), ip, port);
}
else {
_isConnect = true;
//CELLLog_Info("<socket=%d> connect <%s:%d> success...", (int)_pClient->sockfd(), ip, port);
OnConnect(); // 子类根据自己需要具体实现即可,通知子类已经建立连接connect
}
return ret;
}
//关闭套节字closesocket
virtual void Close() // 虚方法由子类重写,实现多态
{
if (_pClient)
{
delete _pClient;
_pClient = nullptr;
}
_isConnect = false;
}
// 处理网络消息, 采用普通虚函数即可, 采用纯虚函数要求子类必须加以实现
virtual bool OnRun(int microseconds = 1) = 0; // 采用参数默认值,调用中没有传参就采用参数默认值
//是否工作中
bool isRun()
{
return _pClient && _isConnect;
}
// 接收数据 处理粘包 拆分包
int RecvData()
{
if (isRun())
{
// 接收客户端数据
int nLen = _pClient->RecvData();
if (nLen > 0)
{
DoMsg(); // 推荐: 具备独立功能的都可封装成独立小函数,函数行数不能超过50行,10行左右最佳
}
return nLen;
}
return 0;
}
// 处理网络消息数据
void DoMsg() // 函数参数个数不宜超过5个
{
// 循环 判断是否有消息需要处理
while (_pClient->hasMsg())
{
// 处理网络消息
OnNetMsg(_pClient->front_msg());
// 移除消息队列(缓冲区)最前的一条数据(队列机制:移除队列头部元素)
_pClient->pop_front_msg();
}
}
//响应网络消息
virtual void OnNetMsg(netmsg_DataHeader* header) = 0;
//发送数据
int SendData(netmsg_DataHeader* header)
{
if (isRun())
return _pClient->SendData(header);
return SOCKET_ERROR;
}
int SendData(const char* pData, int len)
{
if (isRun())
return _pClient->SendData(pData, len);
return SOCKET_ERROR;
}
protected:
// 内部事件分析: 作用域当前类和子类内部,因为外部调用可能会出现错误
// 通知子类已经创建client套接字
virtual void OnInitSocket()
{
}
// 普通虚函数,子类可以有选择性的实现, 具体由其子类实现
virtual void OnConnect()
{
}
protected: // 作用域:当前类内部或子类内部
Client* _pClient = nullptr; // 与具体的网络通信模型无关
bool _isConnect = false;
};
} // namespace io
} // namespace doyou
#endif
\ No newline at end of file
#ifndef _EasyEpollClient_hpp_
#define _EasyEpollClient_hpp_
#if __linux__
#include "TcpClient.hpp"
#include "Epoll.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class TcpEpollClient : public TcpClient // 继承方式:公有继承
{
public:
// 重写父类虚方法
virtual void OnInitSocket() // 在父类中触发当前事件
{
// 创建epoll网络模型实例
_ep.Create(1); // 1个就够了,对于client而言,只会和server进行链接
// 将客户都安套接字socket添加到epoll模型中管理EPOLLIN事件
_ep.Ctl(EPOLL_CTL_ADD, _pClient, EPOLLIN); // EPOLLIN: accept
}
// 主动关闭与服务器的连接
virtual void Close() // 重写父类虚函数
{
_ep.Destory(); // 先释放自己特性资源
TcpClient::Close(); // 再释放继承至父类到自己的资源,显示调用父类函数
}
// 处理网络消息
bool OnRun(int microseconds = 1) // 采用参数默认值,调用中没有传参就采用参数默认值
{
if (isRun())
{
// 需要写数据(存在写入数据)的客户端端才注册send事件
if (_pClient->needWrite()) {
_ep.Ctl(EPOLL_CTL_MOD, _pClient, EPOLLIN | EPOLLOUT); // recv/send
}
else {
_ep.Ctl(EPOLL_CTL_MOD, _pClient, EPOLLIN); // recv
}
int ret = _ep.Wait(microseconds);
if (ret < 0)
{
CELLLog_Error("TcpEpollClient.OnRun.epoll.Wait exit. clientId<%d>, sockfd<%d>", _pClient->id, (int)_pClient->sockfd());
return false; // 函数调用结束
}
else if (ret == 0) // 没有事件,直接返回true
{
return true;
}
// 处理epoll模型中已触发事件
auto events = _ep.Events();
for (int i = 0; i < ret; ++i) { // 遍历事件
Client* pClient = (Client*)events[i].data.ptr; // 取出自定义数据
if (pClient != nullptr) {
if (events[i].events & EPOLLIN) { // recv event happen
if (SOCKET_ERROR == RecvData())
{
CELLLog_Error("<socket=%d>OnRun.epoll RecvData exit", pClient->sockfd());
Close();
continue; // 已经移除异常的客户端,就不要在继续后面逻辑了,避免出问题
}
}
if (events[i].events & EPOLLOUT) { // send event happen
if (SOCKET_ERROR == pClient->SendDataReal())
{
CELLLog_Error("<socket=%d>OnRun.epoll SendDataReal exit", pClient->sockfd());
Close();
}
}
} // if
} // for
return true;
} // if-isRun
return false;
}
protected:
Epoll _ep; // 在栈上实例化一个类的对象
};
} // namespace io
} // namespace doyou
#endif // linux
#endif // header
#ifndef _EasyEpollServer_hpp_
#define _EasyEpollServer_hpp_
#if __linux__ // only linux exist this define
#include "TcpServer.hpp"
#include "EpollServer.hpp"
#include "Epoll.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 继承父类并重写父类:TcpServer
// 当前类是服务器的类,主要职责:接收新的客户端加入
class TcpEpollServer : public TcpServer
{
public:
void Start(int nCELLServer)
{
TcpServer::Start<EpollServer>(nCELLServer); // 强制性调用继承至父类的同名方法 模板函数调用
}
protected:
// private: 作用域当前类内部
// protected: 作用域当前类及其子类内部
// 处理网络消息
void OnRun(Thread* pThread) // 当前函数仅能在函数内部调用
{
Epoll ep;
// 创建epoll网络模型实例
ep.Create(_nMaxClient); // 1个就够了,para1-一次处理事件数量上限
// 将接收新客户端加入的服务端socket添加到epoll模型中管理EPOLLIN事件
ep.Ctl(EPOLL_CTL_ADD, Sockfd(), EPOLLIN); // EPOLLIN: accept
while (pThread->isRun()) // 线程函数里面循环处理
{
time4msg();
int n = ep.Wait(1); // epoll模型检测事件
if (n < 0)
{
CELLLog_Error("TcpEpollServer.OnRun epoll exit."); // 出现异常场景
pThread->Exit();
break;
}
// 处理epoll模型中已触发事件
auto events = ep.Events();
for (int i = 0; i < n; ++i) { // 遍历已触发事件
// equal to server socket , have new client to join
if (events[i].data.fd == Sockfd()) { // _sock // bug?
// fdRead.del(Sockfd());
Accept(); // 接收新客户端连接
}
} // for
} // while
} // onrun
};
} // namespace io
} // namespace doyou
#endif // __linux__
#endif // !_EasyEpollServer_hpp_
#ifndef _EasyIocpClient_hpp_
#define _EasyIocpClient_hpp_
// Iocp模型专用宏
#ifndef CELL_USE_IOCP
#define CELL_USE_IOCP
#endif // CELL_USE_IOCP
#include "TcpClient.hpp"
#include "Iocp.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class TcpIocpClient : public TcpClient // 继承方式:公有继承
{
public:
//
virtual void OnInitSocket() // 在父类中触发当前事件 // 重写父类虚方法
{
_iocp.Create(); // 创建iocp网络模型实例
_iocp.Reg(_pClient->sockfd(), _pClient); // 注册Iocp网络模型对应客户端
}
// 主动关闭与服务器的连接
virtual void Close() // 重写父类虚函数
{
_iocp.Destory(); // 先释放自己特性资源
TcpClient::Close(); // 再释放继承至父类到自己的资源,显示调用父类函数
}
// 处理网络消息
bool OnRun(int microseconds = 1) // 采用参数默认值,调用中没有传参就采用参数默认值
{
if (isRun())
{
// 需要写数据(存在写入数据)的客户端端才注册send事件
if (_pClient->needWrite()) {
// 存在数据:投递发送数据任务
auto pIoData = _pClient->MakeSendIoData();
if (pIoData != nullptr) {
// 分析:发送数据IO_DATA_BASE先传递过去,准备发送数据,发送数据完成后会告诉我们实际发送数据长度
// 准备发送数据, 把发送数据IO_DATA先给它了,并告知缓冲区大小
if (!_iocp.PostSend(pIoData)) {
Close();
return false;
}
}
// 存在数据: 投递接收数据任务
pIoData = _pClient->MakeRecvIoData(); // bug??? 变量重定义
if (pIoData != nullptr) { // 指针在使用前先进行判空处理
// 分析:接收数据IO_DATA_BASE传递过去了,准备接收数据,接收完成后会告诉我们实际接收数据的长度
// 准备接收数据, 把接收数据的IO_DATA给它了,并告知缓冲区大小
if (!_iocp.PostRecv(pIoData)) {
Close();
return false;
}
}
}
else {
// 不存在数据: (仅)投递接收数据任务
auto pIoData = _pClient->MakeRecvIoData();
if (pIoData != nullptr) { // 指针在使用前先进行判空处理
// 分析:接收数据IO_DATA_BASE传递过去了,准备接收数据,接收完成后会告诉我们实际接收数据的长度
// 准备接收数据, 把接收数据的IO_DATA给它了,并告知缓冲区大小
if (!_iocp.PostRecv(pIoData)) {
Close();
return false;
}
}
}
// 分析:满载场景最大存在2个网络事件,接收数据完成事件和发送数据完成事件。
// 采用循环处理方式,一次性就把当前已经发生的网络事件处理完成再继续其它逻辑,基于处理
// 网络事件性能方面,是比较高效的处理方式。
while (true) { // 循环处理事件队列中事件,一次处理一个事件,直到事件队列元素为空(ret=0), 跳出循环
// 待优化点:每次只处理一个事件,事件处理性能较差,需要优化处理。
int ret = DoIocpNetEvents(microseconds); // 接收网络消息数据
if (ret < 0) {
// 分析:相同错误日志有且仅有一次是比较合适的
return false;
}
else if (ret == 0) {
DoMsg(); // 处理网络消息数据
break; // 网络事件队列中所有事件已处理完毕
}
} // while
return true;
} // if-isRun
return false;
}
protected:
// 每次只处理一件网络事件(告诉你这件网络事件完成了-Iocp)
// ret = -1, iocp出错
// ret = 0, 没有事件
// ret = 1, 有事件发生
// 函数返回值:网络事件数量
int DoIocpNetEvents(int microseconds)
{
// 分析:假设模拟10000个客户端端并发,每个客户端等待1ms, 也会等10s,也是比较长的时间
// ,所以在此处,最好没有等待时间。
// 分析2:当前的接收网路事件的存储体设计,同一时间只能存一个事件数据。
int ret = _iocp.Wait(_ioEvent, microseconds);
if (ret < 0) // 模型出错
{
CELLLog_Error("TcpIocpClient.DoIocpNetEvents.Iocp Error exit.clientId<%d>, sockfd<%d>", _pClient->id, (int)_pClient->sockfd());
return ret;
}
else if (ret == 0) // 事件发生数量为0
{
return ret;
}
// 接收数据完成-completion(接收数据完成)
if (IO_TYPE::RECV == _ioEvent.pIOData->iotype) {
// 客户端socket事件触发
if (_ioEvent.bytesTrans <= 0) { // 接收到的数据长度小于或等于0,说明接收数据发送错误(比如远端客户端主动断开连接场景)
CELLLog_Error("TcpIocpClient.DoIocpNetEvents remove client socket[sockClient=%d], IO_TYPE::RECV bytesTrans=[%d].\n", _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
Close();
return -1; // 客户端程序当前类有且仅有一个客户端代理
}
// Reg注册的是:Client*
Client* pClient = (Client*)_ioEvent.data.ptr; // 采用的是c风格的指针类型强制转化
if (pClient != nullptr) {
pClient->Recv4Iocp(_ioEvent.bytesTrans); // 步骤2.0: 回填Iocp实际接收到的数据长度
}
//CELLLog_Info("IO_TYPE::RECV: [sockClient=%d], [bytesTrans=%d].\n", _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
// 分析:在适当实际再次投递接收数据任务,缓冲区有限,在不处理数据的前提下不停提交接收数据任务,很容易让缓冲区填满,后就不能继续接收数据了,有校验的
}
// 发送数据完成-completion(注意:不是开始,而是完成,完成端口,事件做完了才会告诉你)
else if (IO_TYPE::SEND == _ioEvent.pIOData->iotype)
{
if (_ioEvent.bytesTrans <= 0) { // 发送数据长度小于或等于0,说明发送数据错误, 比如远端客户端主动断开连接场景
CELLLog_Error("TcpIocpClient.DoIocpNetEvents remove client socket[sockClient=%d], IO_TYPE::SEND bytesTrans=[%d]."
, _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
Close();
return -1;
}
// Reg注册的数据类型是:Client*
Client* pClient = (Client*)_ioEvent.data.ptr;
if (pClient != nullptr) {
pClient->Send2Iocp(_ioEvent.bytesTrans); // 回填Iocp实际发送的数据长度
}
//CELLLog_Info("IO_TYPE::SEND: [sockClient=%d], [bytesTrans=%d]."
//, _ioEvent.pIOData->sockfd, _ioEvent.bytesTrans);
}
else {
CELLLog_Warring("undefine io_type.");
}
return ret;
}
protected:
Iocp _iocp; // 在栈上实例化一个类的对象
IO_EVENT _ioEvent; // 类属性(与之对应有类方法) // 分析:编译器比较旧不支持类的属性在声明时候进行初始化操作
};
} // namespace io
} // namespace doyou
#endif
#ifndef _EasyIocpServer_hpp_
#define _EasyIocpServer_hpp_
// 分析:IocpServer.hpp使用文件为当前文件TcpIocpServer.hpp
#ifndef CELL_USE_IOCP // Iocp模型专用宏
#define CELL_USE_IOCP
#endif // CELL_USE_IOCP
#include "TcpServer.hpp"
#include "IocpServer.hpp"
//#include "SelectServer.hpp" // 暂用
#include "Iocp.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class TcpIocpServer : public TcpServer
{
public:
void Start(int nCELLServer)
{
// 注意:接收网络消息和发送网络消息暂用select网络通信模型
TcpServer::Start<IocpServer>(nCELLServer); // 暂用:SelectServer
}
protected:
// 处理网络消息
void OnRun(Thread* pThread)
{
Iocp iocp;
iocp.Create();
iocp.Reg(Sockfd());
iocp.LoadAcceptEx(Sockfd());
// 分析:此处给定Iocp连接的缓冲区,对于接收和发送网络数据的缓冲区就要关联Client开辟的缓冲区了,以兼容系统框架
//const int len = 2 * (sizeof(sockaddr_in) + 16); // 本端地址+远端地址 长度和
// 不需要客户端连接后再立即发送数据的情况下的最低长度len
const int len = 1024; // 常量:被赋初值后,不能在重新赋值, 空间给足,因为服务端套接字一般就一个而已
char buf[len] = {}; // 数组也是在栈空间实例化的对象,无需手动释放资源
IO_DATA_BASE ioData = {}; // 采用结构体的初始化器对结构体对象进行初始化
ioData.wsabuff.buf = buf;
ioData.wsabuff.len = len;
iocp.PostAccept(&ioData);
IO_EVENT ioEvent = {};
while (pThread->isRun())
{
time4msg();
int n = iocp.Wait(ioEvent, 1);
if (n < 0)
{
CELLLog_Error("TcpIocpServer.OnRun iocp exit."); // 程序出现错误,要记录错误日志
pThread->Exit();
break; // 程序异常,结束程序
}
if (n == 0) {
continue; // 当前没有事件发生,停止当前循环,继续下次循环
}
// 接收链接完成
if (IO_TYPE::ACCEPT == ioEvent.pIOData->iotype) { // 完成键返回数据是sockServer,说明服务端有新客户端加入
// 服务端socket事件触发
CELLLog_Info("new client[sockfd=%d] to join.\n", ioEvent.pIOData->sockfd); // 目前从返回的数据中无法获取其它信息,因为只传入了sockServer的信息
IocpAccept(ioEvent.pIOData->sockfd); // 客户端socket已知
// 待优化点:投递接收任务前,先校验已经连接的客户端总数是否超过连接上限
// 继续 向Iocp投递接收连接的任务
iocp.PostAccept(&ioData);
}
} // while
} // onrun
// Iocp接受客户端连接
SOCKET IocpAccept(SOCKET cSock)
{
// 新连接的客户端的IP地址和端口号PORT, 暂时不需要,后期业务有需要再传递过来
//sockaddr_in clientAddr = {};
//int nAddrLen = sizeof(sockaddr_in);
if (INVALID_SOCKET == cSock)
{
// errno-错误码 strerror-错误码对应描述信息
// 接收到的新客户端套接字无效
CELLLog_PError("accept INVALID_SOCKET...errno<%d> errMsg<%s>", errno, strerror(errno));
}
else
{
//CELLLog_Info("accept cSock<%d>", cSock);
// 客户端链接上限校验
if (_clientAccept < _nMaxClient)
{
_clientAccept++; // 实时统计已连接客户端数量(继承至父类属性)
NetWork::make_reuseaddr(cSock);
// 将新客户端分配给客户数量最少的cellServer
addClientToCELLServer(new Client(cSock, _nSendBuffSize, _nRecvBuffSize));
//获取IP地址 inet_ntoa(clientAddr.sin_addr)
}
else {
// 获取IP地址 inet_ntoa(clientAddr.sin_addr)
// 分析:超出链接上限,先链接再关闭:
// 好处1)可以知道是谁链接我们了(客户端信息)。好处2)做一些其它操作,比如通知客户端我超限制了
// 好处3)可以查看该客户端是否在黑名单或白名单中,是否有传指定验证码token, 客户端需要传入一个正确的检验码。
// 如果重复多次链接,有可能是恶意链接。
NetWork::destroy_socket(cSock);
CELLLog_Warring("Accept to nMaxClient");
}
}
return cSock;
}
};
} // namespace io
} // namespace doyou
#endif // !_EasyIocpServer_hpp_
#ifndef _EasySelectClient_hpp_
#define _EasySelectClient_hpp_
#include "TcpClient.hpp"
#include "FDSet.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class TcpSelectClient : public TcpClient // 继承方式:共有继承
{
public:
//
TcpSelectClient() // 构造函数会在创建对象的过程中自动调用
{
_fdRead.Create(1);
_fdWrite.Create(1);
}
// 处理网络消息
bool OnRun(int microseconds = 1) // 采用参数默认值,调用中没有传参就采用参数默认值
{
if (isRun())
{
SOCKET _sock = _pClient->sockfd();
_fdRead.zero();
_fdRead.add(_sock);
_fdWrite.zero();
timeval t = { 0, microseconds };
int ret = 0;
if (_pClient->needWrite())
{
_fdWrite.add(_sock);
ret = select(_sock + 1, _fdRead.fdset(), _fdWrite.fdset(), nullptr, &t);
}
else {
ret = select(_sock + 1, _fdRead.fdset(), nullptr, nullptr, &t);
}
if (ret < 0) // 出现异常
{
// 分析:一般而言,属性不能直接对外暴露。应封装在类的内部。
CELLLog_Error("TcpSelectClient.OnRun.select Error exit. clientId<%d>, sockfd<%d>", _pClient->id, (int)_sock);
Close();
return false;
}
if (_fdRead.has(_sock)) // 此socket接收到网络消息-recv
{
if (SOCKET_ERROR == RecvData())
{
CELLLog_Error("<socket=%d>OnRun.select RecvData exit", (int)_sock);
Close();
return false;
}
}
if (_fdWrite.has(_sock)) // 此socket有数据发送,可写-send
{
if (SOCKET_ERROR == _pClient->SendDataReal())
{
CELLLog_Error("<socket=%d>OnRun.select SendDataReal exit", (int)_sock);
Close();
return false;
}
}
return true;
}
return false;
}
protected:
FDSet _fdRead;
FDSet _fdWrite;
};
} // namespace io
} // namespace doyou
#endif
#ifndef _EasySelectServer_hpp_
#define _EasySelectServer_hpp_
#include "TcpServer.hpp"
#include "SelectServer.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 继承父类并重写父类:TcpServer
class TcpSelectServer : public TcpServer
{
public:
void Start(int nCELLServer)
{
TcpServer::Start<SelectServer>(nCELLServer); // 强制性调用继承至父类的同名方法 模板函数调用
}
protected:
// private: 作用域当前类内部
// protected: 当前类或子类内部可用
// 处理网络消息
void OnRun(Thread* pThread) // 当前函数仅能在函数内部调用
{
// 伯克利套接字 BSD socket
// 描述符(socket) 集合
// 分析:将这个操作从循环体抽出可以提高系统性能,因为高频繁的申请释放对象比较消耗系统资源
FDSet fdRead;
fdRead.Create(_nMaxClient); // 该实参数据源是配置文件配置数据
while (pThread->isRun())
{
time4msg();
// 清理集合
fdRead.zero();
// 将描述符(socket)加入集合
fdRead.add(Sockfd());
/// nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
/// para1-既是所有文件描述符最大值+1 在Windows中这个参数可以写0
timeval t = { 0, 1 };
int ret = select(Sockfd() + 1, fdRead.fdset(), 0, 0, &t); //
if (ret < 0)
{
if (errno == EINTR) { // 处理可能出现的系统终端导致的程序终止
//CELLLog_PError("TcpSelectServer.OnRun select exit, deal errno == EINTR");
continue; // 忽略系统终端信号[EINTR], 程序继续正常运行
}
CELLLog_PError("TcpSelectServer.OnRun select exit."); // select is socketAPI
pThread->Exit();
break;
}
// 判断描述符(socket)是否在集合中
// 问题: 使用并没有区分是win还是linux,具体实现是采用win里面的宏函数,在linux下面岂不是函数实现为空吗
// 解答:不同的操作系统下面都有一样的宏FD_ISSET,这样使用的时候才不用进行系统区别处理,但是相同的宏可能在不同的操作系统的具体实现可能存在差异
if (fdRead.has(Sockfd()))
{
// fdRead.del(_sock);
Accept();
}
}
}
};
} // namespace io
} // namespace doyou
#endif // !_EasySelectServer_hpp_
#ifndef _EasyTcpServer_hpp_
#define _EasyTcpServer_hpp_
#include"CELL.hpp"
#include"Client.hpp"
#include"Server.hpp"
#include"INetEvent.hpp"
#include"NetWork.hpp"
#include"Config.hpp"
// 分析:项目中不必要的头文件不要包含进来,会增大可执行文件大小
//#include"FDSet.hpp" // 封装的fd_set集合
#include<thread>
#include<mutex>
#include<atomic>
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
// 类职责: 服务端接收新客户端链接
class TcpServer : public INetEvent
{
private:
//
Thread _thread;
//消息处理对象,内部会创建线程
std::vector<Server*> _cellServers;
//每秒消息计时
Timestamp _tTime;
// 服务端套接字,功能:接收新的客户端加入
SOCKET _sock;
protected:
// 客户端发送缓冲区大小
int _nSendBuffSize;
// 客户端接收缓冲区大小
int _nRecvBuffSize;
// 客户端连接上限
int _nMaxClient;
//SOCKET recv计数
std::atomic_int _recvCount;
//收到消息计数
std::atomic_int _msgCount;
// (已连接)客户端计数 = 连接已分配总数 + 连接未分配总数
std::atomic_int _clientAccept;
// 已分配(给处理接收和发送网络消息线程)客户端计数
std::atomic_int _clientJoin; // 原子数据,由多个线程操作也可确保数据的正确性
public:
TcpServer() // 构造函数给对象属性进行初始化
{
_sock = INVALID_SOCKET;
_recvCount = 0;
_msgCount = 0;
_clientAccept = 0;
_clientJoin = 0;
_nSendBuffSize = Config::Instance().getInt("nSendBuffSize", SEND_BUFF_SZIE); // 读取shell脚本传递进来的参数值
_nRecvBuffSize = Config::Instance().getInt("nRecvBuffSize", RECV_BUFF_SZIE);
_nMaxClient = Config::Instance().getInt("nMaxClient", FD_SETSIZE);
}
virtual ~TcpServer()
{
Close();
}
// 初始化Socket(服务端程序)
SOCKET InitSocket()
{
NetWork::Init();
if (INVALID_SOCKET != _sock)
{
CELLLog_Warring("initSocket close old socket<%d>...", (int)_sock);
Close();
}
_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == _sock)
{
CELLLog_PError("create socket failed...");
}
else {
// 兼容场景:关闭socket并立即生效释放端口资源,可以立即对刚关闭socket套接字的端口进行重新使用
NetWork::make_reuseaddr(_sock); // 暂时注释,测试打印错误信息
CELLLog_Info("create socket<%d> success...", (int)_sock);
}
return _sock;
}
//绑定IP和端口号
int Bind(const char* ip, unsigned short port)
{
//if (INVALID_SOCKET == _sock)
//{
// InitSocket();
//}
// 2 bind 绑定用于接受客户端连接的网络端口
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(port);//host to net unsigned short
#ifdef _WIN32
if (ip){
_sin.sin_addr.S_un.S_addr = inet_addr(ip);
}
else {
_sin.sin_addr.S_un.S_addr = INADDR_ANY;
}
#else
if (ip) {
_sin.sin_addr.s_addr = inet_addr(ip);
}
else {
_sin.sin_addr.s_addr = INADDR_ANY;
}
#endif
int ret = bind(_sock, (sockaddr*)&_sin, sizeof(_sin));
if (SOCKET_ERROR == ret)
{
// GetLastError(): windows系统获取错误时机的错误码,可在网上搜罗错误码的错误含义或者差错误手册
//CELLLog_Error("bind port<%d> failed... errno<%d>,errmsg<%s>", port, GetLastError(), strerror(errno));
CELLLog_PError("bind port<%d> failed...", port);
}
else {
CELLLog_Info("bind port<%d> success...", port);
}
return ret;
}
//监听端口号
int Listen(int n)
{
// 3 listen 监听网络端口
int ret = listen(_sock, n);
if (SOCKET_ERROR == ret)
{
CELLLog_PError("listen socket<%d> failed...", _sock); // 采用PError可以输出错误码和错误原因
}
else {
CELLLog_Info("listen port<%d> success...", _sock);
}
return ret;
}
// 接受客户端连接
SOCKET Accept()
{
// 4 accept 等待接受客户端连接
sockaddr_in clientAddr = {};
int nAddrLen = sizeof(sockaddr_in);
SOCKET cSock = INVALID_SOCKET;
#ifdef _WIN32
cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
#else
cSock = accept(_sock, (sockaddr*)&clientAddr, (socklen_t *)&nAddrLen);
#endif
if (INVALID_SOCKET == cSock)
{
// errno-错误码 strerror-错误码对应描述信息
// 接收到的新客户端套接字无效
CELLLog_PError("accept INVALID_SOCKET...errno<%d> errMsg<%s>", errno, strerror(errno));
}
else
{
//CELLLog_Info("accept cSock<%d>", cSock);
// 客户端链接上限校验
if (_clientAccept < _nMaxClient)
{
_clientAccept++; // 连接的新客户端被分配给Server,计数+1
NetWork::make_reuseaddr(cSock);
// 将新客户端分配给客户数量最少的cellServer
addClientToCELLServer(new Client(cSock, _nSendBuffSize, _nRecvBuffSize));
// 获取IP地址 inet_ntoa(clientAddr.sin_addr)
}
else {
// 获取IP地址 inet_ntoa(clientAddr.sin_addr)
// 分析:超出链接上限,先链接再关闭:
// 好处1)可以知道是谁链接我们了(客户端信息)。好处2)做一些其它操作,比如通知客户端我超限制了
// 好处3)可以查看该客户端是否在黑名单或白名单中,是否有传指定验证码token, 客户端需要传入一个正确的检验码。
// 如果重复多次链接,有可能是恶意链接。
NetWork::destroy_socket(cSock);
CELLLog_Warring("Accept to nMaxClient");
}
}
return cSock;
}
void addClientToCELLServer(Client* pClient)
{
//查找客户数量最少的Server消息处理对象
auto pMinServer = _cellServers[0];
for (auto pServer : _cellServers)
{
if (pMinServer->getClientCount() > pServer->getClientCount())
{
pMinServer = pServer;
}
}
pMinServer->addClient(pClient);
}
// 函数模型:区分使用SelectServer和EpollServer
template<class ServerT>
void Start(int nCELLServer)
{
for (int n = 0; n < nCELLServer; n++)
{
auto ser = new ServerT(); // SelectServer or EpollServer
ser->SetId(n + 1); // 调用继承自父类的方法
ser->SetClientNum(_nMaxClient / nCELLServer + 1); // +1兼容没整除存在余数的场景
_cellServers.push_back(ser);
//注册网络事件接受对象
ser->setEventObj(this);
//启动消息处理线程
ser->Start();
}
_thread.Start(nullptr,
[this](Thread* pThread) {
OnRun(pThread); // OnRun函数调用位置
});
}
//关闭Socket
void Close()
{
CELLLog_Info("TcpServer.Close begin");
_thread.Close();
if (_sock != INVALID_SOCKET)
{
for (auto s : _cellServers)
{
delete s;
}
_cellServers.clear();
NetWork::destroy_socket(_sock);
_sock = INVALID_SOCKET;
}
CELLLog_Info("TcpServer.Close end");
}
//cellServer 4 多个线程触发 不安全
//如果只开启1个cellServer就是安全的
virtual void OnNetJoin(Client* pClient)
{
_clientJoin++;
//CELLLog_Info("client<%d> join", pClient->sockfd());
}
//cellServer 4 多个线程触发 不安全
//如果只开启1个cellServer就是安全的
virtual void OnNetLeave(Client* pClient)
{
_clientAccept--;
_clientJoin--;
//CELLLog_Info("client<%d> leave", pClient->sockfd());
}
//cellServer 4 多个线程触发 不安全
//如果只开启1个cellServer就是安全的
virtual void OnNetMsg(Server* pServer, Client* pClient, netmsg_DataHeader* header)
{
_msgCount++;
}
virtual void OnNetRecv(Client* pClient)
{
_recvCount++;
//CELLLog_Info("client<%d> leave", pClient->sockfd());
}
protected:
// protected: 作用域当前类及其子类内部
// 处理网络消息
virtual void OnRun(Thread* pThread) = 0; // 纯虚函数
// 计算并输出每秒收到的网络消息
void time4msg()
{
auto t1 = _tTime.getElapsedSecond();
if (t1 >= 1.0) // 每秒统计输出
{
CELLLog_Info("thread<%d>,time<%lf>,socket<%d>,Accept<%d>,Join<%d>,recv<%d>,msg<%d>" // 超过一行显示宽度120推荐采用多行显示
, (int)_cellServers.size()
, t1
, _sock
, (int)_clientAccept
, (int)_clientJoin
, (int)_recvCount
, (int)_msgCount);
_recvCount = 0;
_msgCount = 0;
_tTime.update();
}
}
//
SOCKET Sockfd() // 类属性封装
{
return _sock;
}
};
} // namespace io
} // namespace doyou
#endif // !_EasyTcpServer_hpp_
#ifndef _CELL_THREAD_HPP_
#define _CELL_THREAD_HPP_
#include"Semaphore.hpp"
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Thread
{
public:
static void Sleep(time_t dt)
{
std::chrono::milliseconds t(dt);
std::this_thread::sleep_for(t);
}
private:
typedef std::function<void(Thread*)> EventCall;
public:
//启动线程
void Start(
EventCall onCreate = nullptr,
EventCall onRun = nullptr,
EventCall onDestory = nullptr)
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_isRun)
{
_isRun = true;
if (onCreate)
_onCreate = onCreate;
if (onRun)
_onRun = onRun;
if (onDestory)
_onDestory = onDestory;
//线程
std::thread t(std::mem_fn(&Thread::OnWork), this);
t.detach();
}
}
//关闭线程
void Close()
{
std::lock_guard<std::mutex> lock(_mutex);
if (_isRun)
{
_isRun = false;
_sem.wait();
}
}
//在工作函数中退出
//不需要使用信号量来阻塞等待
//如果使用会阻塞
void Exit()
{
std::lock_guard<std::mutex> lock(_mutex);
if (_isRun)
{
_isRun = false;
}
}
//线程是否启动运行状态
bool isRun()
{
return _isRun;
}
protected:
//线程的运行时的工作函数
void OnWork()
{
if (_onCreate)
_onCreate(this);
if (_onRun)
_onRun(this);
if (_onDestory)
_onDestory(this);
_sem.wakeup();
_isRun = false;
}
private:
EventCall _onCreate;
EventCall _onRun;
EventCall _onDestory;
//不同线程中改变数据时需要加锁
std::mutex _mutex;
//控制线程的终止、退出
Semaphore _sem;
//线程是否启动运行中
bool _isRun = false;
};
} // namespace io
} // namespace doyou
#endif // !_CELL_THREAD_HPP_
#ifndef _CELLTimestamp_hpp_
#define _CELLTimestamp_hpp_
//#include <windows.h>
#include<chrono>
using namespace std::chrono;
namespace doyou {
namespace io { // 增加两层命令空间,避免在其它项目应用时类名出现冲突
class Time
{
public:
//获取当前时间戳 (毫秒)
static time_t getNowInMilliSec()
{
return duration_cast<milliseconds>(high_resolution_clock::now().time_since_epoch()).count();
}
};
class Timestamp
{
public:
Timestamp()
{
//QueryPerformanceFrequency(&_frequency);
//QueryPerformanceCounter(&_startCount);
update();
}
~Timestamp()
{}
void update()
{
//QueryPerformanceCounter(&_startCount);
_begin = high_resolution_clock::now();
}
/**
* 获取当前秒
*/
double getElapsedSecond()
{
return getElapsedTimeInMicroSec() * 0.000001;
}
/**
* 获取毫秒
*/
double getElapsedTimeInMilliSec()
{
return this->getElapsedTimeInMicroSec() * 0.001;
}
/**
* 获取微妙
*/
long long getElapsedTimeInMicroSec()
{
/*
LARGE_INTEGER endCount;
QueryPerformanceCounter(&endCount);
double startTimeInMicroSec = _startCount.QuadPart * (1000000.0 / _frequency.QuadPart);
double endTimeInMicroSec = endCount.QuadPart * (1000000.0 / _frequency.QuadPart);
return endTimeInMicroSec - startTimeInMicroSec;
*/
return duration_cast<microseconds>(high_resolution_clock::now() - _begin).count();
}
protected:
//LARGE_INTEGER _frequency;
//LARGE_INTEGER _startCount;
time_point<high_resolution_clock> _begin;
};
} // namespace io
} // namespace doyou
#endif // !_CELLTimestamp_hpp_
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Text Include="Record.txt">
<Filter>源文件</Filter>
</Text>
<Text Include="Record.txt" />
</ItemGroup>
</Project>
\ No newline at end of file
// 高性能网络通信服务器2.0关键技术点总结DOC
// 开始时间[2023-3-8 to 至今]
1.消息缓冲区设置大小应该依据消息size来加以考虑。
......@@ -11,7 +11,7 @@
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{8BAC00A4-A941-4002-8B0D-EC863407F441}</ProjectGuid>
<ProjectGuid>{337E0C8A-36D4-428C-98CA-BAB56FA59CE9}</ProjectGuid>
<RootNamespace>EasyClient</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
......@@ -39,14 +39,19 @@
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)../bin/$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)../temp/$(Configuration)\</IntDir>
<OutDir>$(SolutionDir)\..\bin\$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)\..\tmp\$(Configuration)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)\..\bin\$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)\..\tmp\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<SDLCheck>false</SDLCheck>
<AdditionalIncludeDirectories>..\Depends\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
......@@ -58,7 +63,8 @@
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<SDLCheck>false</SDLCheck>
<AdditionalIncludeDirectories>..\Depends\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
......@@ -69,6 +75,9 @@
<ItemGroup>
<ClCompile Include="client.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\bin\Debug\TcpEasyClient_wins_ipv4.bat" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
......
......@@ -19,4 +19,9 @@
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="..\..\bin\Debug\TcpEasyClient_wins_ipv4.bat">
<Filter>头文件</Filter>
</None>
</ItemGroup>
</Project>
\ No newline at end of file
......@@ -11,9 +11,8 @@
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{F6FBA505-243A-4A6F-BABE-2370F66EF42D}</ProjectGuid>
<RootNamespace>engine20</RootNamespace>
<ProjectName>EasyServer</ProjectName>
<ProjectGuid>{ADBF9965-7E47-472B-B306-9CD7BC1105E9}</ProjectGuid>
<RootNamespace>EasyServer</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
......@@ -41,13 +40,18 @@
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)../bin/$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)../temp/$(Configuration)\</IntDir>
<IntDir>$(SolutionDir)../tmp/$(Configuration)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)../bin/$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)../tmp/$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<SDLCheck>false</SDLCheck>
<AdditionalIncludeDirectories>..\Depends\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
......@@ -59,7 +63,8 @@
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<SDLCheck>false</SDLCheck>
<AdditionalIncludeDirectories>..\Depends\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
......@@ -70,6 +75,9 @@
<ItemGroup>
<ClCompile Include="server.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\bin\Debug\TcpEasyServer_wins_ipv4.bat" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
......
......@@ -19,4 +19,9 @@
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="..\..\bin\Debug\TcpEasyServer_wins_ipv4.bat">
<Filter>头文件</Filter>
</None>
</ItemGroup>
</Project>
\ No newline at end of file
此差异已折叠。
<?xml version="1.0" encoding="utf-8"?>
<ClassDiagram />
\ No newline at end of file
生成启动时间为 2023/3/8 18:30:54。
生成成功。
已用时间 00:00:00.02
......@@ -11,8 +11,8 @@
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{871A243C-84FC-49FC-94FF-F88C1C51FFC9}</ProjectGuid>
<RootNamespace>Doc</RootNamespace>
<ProjectGuid>{6F51632A-CEC0-44E3-93F5-0757720FA96D}</ProjectGuid>
<RootNamespace>engine10</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
......@@ -64,7 +64,35 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<Text Include="Record.txt" />
<ClInclude Include="..\Depends\include\Buffer.hpp" />
<ClInclude Include="..\Depends\include\ByteStream.hpp" />
<ClInclude Include="..\Depends\include\CELL.hpp" />
<ClInclude Include="..\Depends\include\Client.hpp" />
<ClInclude Include="..\Depends\include\Config.hpp" />
<ClInclude Include="..\Depends\include\Epoll.hpp" />
<ClInclude Include="..\Depends\include\EpollServer.hpp" />
<ClInclude Include="..\Depends\include\FDSet.hpp" />
<ClInclude Include="..\Depends\include\INetEvent.hpp" />
<ClInclude Include="..\Depends\include\Iocp.hpp" />
<ClInclude Include="..\Depends\include\IocpServer.hpp" />
<ClInclude Include="..\Depends\include\Log.hpp" />
<ClInclude Include="..\Depends\include\MessageHeader.hpp" />
<ClInclude Include="..\Depends\include\MsgStream.hpp" />
<ClInclude Include="..\Depends\include\NetWork.hpp" />
<ClInclude Include="..\Depends\include\SelectServer.hpp" />
<ClInclude Include="..\Depends\include\Semaphore.hpp" />
<ClInclude Include="..\Depends\include\Server.hpp" />
<ClInclude Include="..\Depends\include\Task.hpp" />
<ClInclude Include="..\Depends\include\TcpClient.hpp" />
<ClInclude Include="..\Depends\include\TcpEpollClient.hpp" />
<ClInclude Include="..\Depends\include\TcpEpollServer.hpp" />
<ClInclude Include="..\Depends\include\TcpIocpClient.hpp" />
<ClInclude Include="..\Depends\include\TcpIocpServer.hpp" />
<ClInclude Include="..\Depends\include\TcpSelectClient.hpp" />
<ClInclude Include="..\Depends\include\TcpSelectServer.hpp" />
<ClInclude Include="..\Depends\include\TcpServer.hpp" />
<ClInclude Include="..\Depends\include\Thread.hpp" />
<ClInclude Include="..\Depends\include\TimeStamp.hpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
......
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Depends\include\Buffer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\ByteStream.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\CELL.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Client.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Config.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Epoll.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\EpollServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\FDSet.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\INetEvent.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Iocp.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\IocpServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Log.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\MessageHeader.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\MsgStream.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\NetWork.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\SelectServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Semaphore.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Server.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Task.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpClient.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpEpollClient.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpEpollServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpIocpClient.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpIocpServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpSelectClient.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpSelectServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TcpServer.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\Thread.hpp">
<Filter>源文件</Filter>
</ClInclude>
<ClInclude Include="..\Depends\include\TimeStamp.hpp">
<Filter>源文件</Filter>
</ClInclude>
</ItemGroup>
</Project>
\ No newline at end of file
此差异已折叠。
#pragma once
#pragma execution_character_set("utf-8")
int main()
{
return 0;
}
e:\files\技术doc\engine2.0\engine2.0\engine2.0\temp\debug\server.obj
e:\files\技术doc\engine2.0\engine2.0\engine2.0\temp\debug\vc120.idb
e:\files\技术doc\engine2.0\engine2.0\engine2.0\temp\debug\vc120.pdb
生成启动时间为 2023/3/4 9:39:50。
1>项目“E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\EasyClient\EasyClient.vcxproj”在节点 2 上(Rebuild 个目标)。
1>C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V120\Microsoft.CppBuild.targets(388,5): warning MSB8028: The intermediate directory (E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../temp/Debug\) contains files shared from another project (EasyServer.vcxproj). This can lead to incorrect clean and rebuild behavior.
1>ClCompile:
D:\software\VC\bin\CL.exe /c /ZI /nologo /W3 /WX- /sdl /Od /Oy- /D _MBCS /Gm /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fo"E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../temp/Debug\\" /Fd"E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../temp/Debug\vc120.pdb" /Gd /TP /analyze- /errorReport:prompt client.cpp
client.cpp
Link:
D:\software\VC\bin\link.exe /ERRORREPORT:PROMPT /OUT:"E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../bin/Debug\EasyClient.exe" /INCREMENTAL /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../bin/Debug\EasyClient.pdb" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../bin/Debug\EasyClient.lib" /MACHINE:X86 "E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../temp/Debug\client.obj"
EasyClient.vcxproj -> E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\../bin/Debug\EasyClient.exe
1>已完成生成项目“E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\EasyClient\EasyClient.vcxproj”(Rebuild 个目标)的操作。
生成成功。
已用时间 00:00:00.80
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/CL.read.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/CL.write.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/cl.command.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/link.command.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/link.read.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyClient.tlog/link.write.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/CL.read.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/CL.write.1.tlog and /dev/null differ
#TargetFrameworkVersion=v4.0:PlatformToolSet=v120:EnableManagedIncrementalBuild=false:VCToolArchitecture=Native32Bit
Debug|Win32|E:\files\技术doc\engine2.0\engine2.0\engine2.0\engine2.0\|
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/cl.command.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/link.command.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/link.read.1.tlog and /dev/null differ
Binary files a/engine2.0/temp/Debug/EasyServer.tlog/link.write.1.tlog and /dev/null differ
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册