笔记一:建立基础TCP服务端/客户端 点我跳转
笔记二:网络数据报文的收发 点我跳转
笔记三:升级为select网络模型 点我跳转
笔记四:跨平台支持Windows、Linux系统 点我跳转
笔记五:源码的封装 点我跳转
笔记六:缓冲区溢出与粘包分包 点我跳转
笔记七:服务端多线程分离业务处理高负载 点我跳转
笔记八:对socket select网络模型的优化 点我跳转
笔记九:消息接收与发送分离 点我跳转
笔记十:项目化 (加入内存池静态库 / 报文动态库)
之前的客户端虽然可以跑起来,但是声明和实现全写于一个hpp文件中,随着代码日渐增多,增删改变得越发困难。所以我决定尝试将其实现的更加标准。本次我准备的内容如下:
我首先准备将之前服务端源代码的hpp文件进行分离,分离成单独类声明与实现。且服务端本体源码放在一个项目中。随后新建静态库项目存储内存池源码,使服务端源码项目链接内存池静态库。最后新建一个动态库项目,里面存放计时器类代码和报文CMD文件,因为服务端和客户端程序都要用这个库里的文件,为了今后方便改动,我选择使用动态库。
首先,服务端源码按功能可分为五个部分:
按照五个部分的关系等,可画出如下的关系图:
由此,我令 ⑤部分 include ①③④部分,再令 ②部分 include ⑤部分,即可实现项目各文件间的关联。
INetEvent.h
/*
* 本文件中定义了服务端的基础接口
* 在TcpServer.h中服务端类继承了该接口
*
* 目前仅定义了四个基础事件
* 2021/4/22
*/
#ifndef _INET_EVENT_H_
#define _INET_EVENT_H_
//相关预声明
class ClientSocket;
class CellServer;
struct DataHeader;
//服务端基础接口
class INetEvent
{
public:
//客户端退出事件
virtual void OnNetJoin(ClientSocket* pClient) = 0;
//客户端退出事件
virtual void OnNetLeave(ClientSocket* pClient) = 0;
//服务端发送消息事件
virtual void OnNetMsg(CellServer* pCellServer, ClientSocket* pClient, DataHeader* pHead) = 0;
//服务端接收消息事件
virtual void OnNetRecv(ClientSocket* pClient) = 0;
};
#endif
TcpServer.h
/*
* 服务端类
* 实现基础的socket连接操作
* 通过start方法生成子线程 监控收包
* 主线程仅进行客户端加入退出监控
* 2021/4/22
*/
#ifndef _TCP_SERVER_H_
#define _TCP_SERVER_H_
#include"CellServer.h"
#include
#include
//服务端类
class TcpServer : INetEvent
{
public:
//构造
TcpServer();
//析构
virtual ~TcpServer();
//初始化socket 返回1为正常
int InitSocket();
//绑定IP/端口
int Bind(const char* ip, unsigned short port);
//监听端口
int Listen(int n);
//接受连接
int Accept();
//关闭socket
void CloseSocket();
//添加客户端至服务端
void AddClientToServer(ClientSocket* pClient);
//线程启动
void Start(int nCellServer);
//判断是否工作中
inline bool IsRun();
//查询是否有待处理消息
bool OnRun();
//显示各线程数据信息
void time4msg();
//客户端加入事件
virtual void OnNetJoin(ClientSocket* pClient);
//客户端退出
virtual void OnNetLeave(ClientSocket* pClient);
//客户端发送消息事件
virtual void OnNetMsg(CellServer* pCellServer, ClientSocket* pClient, DataHeader* pHead);
virtual void OnNetRecv(ClientSocket* pClient);
private:
//socket相关
SOCKET _sock;
std::vector<CellServer*> _cellServers;//线程处理
//计时器
mytimer _time;
//发送包的数量
std::atomic_int _msgCount;
//接收包的数量
std::atomic_int _recvCount;
//客户端计数
std::atomic_int _clientCount;
};
#endif
ClientSocket.h
/*
* 客户端类
* 服务端对象中每加入一个新的客户端,都会新建一个客户端对象
* 通过该客户端对象向客户端进行发送消息等操作
*
* 目前来说只实现了定量发送数据,即发送缓冲区满后发送消息,下一步预备完善为定时定量发送信息
* 2021/4/22
*/
#ifndef _CLIENT_SOCKET_H_
#define _CLIENT_SOCKET_H_
//socket相关内容
#ifdef _WIN32
#define FD_SETSIZE 1024
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")//链接此动态链接库 windows特有
//连接动态库 此动态库里含有计时器类timer 和 cmd命令
#include "pch.h"
#pragma comment(lib, "guguDll.lib")
//连接静态库 此静态库里含有一个内存池
#pragma comment(lib, "guguAlloc.lib")
#else
#include
#include //selcet
#include //uni std
#include
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
//缓冲区大小
#ifndef RECV_BUFFER_SIZE
#define RECV_BUFFER_SIZE 4096
#define SEND_BUFFER_SIZE 40
#endif
//客户端类
class ClientSocket
{
public:
//构造
ClientSocket(SOCKET sockfd = INVALID_SOCKET);
//析构
virtual ~ClientSocket();
//获取socket
SOCKET GetSockfd();
//获取接收缓冲区
char* MsgBuf();
//获取接收缓冲区尾部变量
int GetLen();
//设置缓冲区尾部变量
void SetLen(int len);
//发送数据
int SendData(DataHeader* head);
private:
SOCKET _sockfd;
//缓冲区相关
char* _Msg_Recv_buf;//消息缓冲区
int _Len_Recv_buf;//缓冲区数据尾部变量
char* _Msg_Send_buf;//消息发送缓冲区
int _Len_Send_buf;//发送缓冲区数据尾部变量
};
#endif
CellTask.h
/*
* 子线程发送部分
* 本头文件中实现了任务执行的分离操作
* 通过list结构存储需要执行的任务
* start()启动线程进行任务处理
* 为防止出现冲突,所有临界操作均进行上锁,且首先使用缓冲区储存新任务
*
* 2021/4/22
*/
#ifndef _CELL_Task_hpp_
#define _CELL_Task_hpp_
#include
#include
#include
#include //mem_fn
//任务基类接口
class CellTask
{
public:
//执行任务
virtual void DoTask() = 0;
};
//发送线程类
class CellTaskServer
{
public:
CellTaskServer();
virtual ~CellTaskServer();
//添加任务
void addTask(CellTask* ptask);
//启动服务
void Start();
protected:
//工作函数
void OnRun();
private:
//任务数据
std::list<CellTask*>_tasks;
//任务数据缓冲区
std::list<CellTask*>_tasksBuf;
//锁 锁数据缓冲区
std::mutex _mutex;
};
#endif
CellServer.h
/*
* 子线程接收部分
* 本文件中实现线程类以及DoTask接口
* 使服务端线程分离 主线程接收连接 其余子线程处理消息
*
* 初步实现了发送任务的接口,使其调用客户端对象的SendData方法发送消息
* 目前采用的是select结构 未来可能尝试其他结构
* 2021/4/22
*/
#ifndef _CELL_SERVER_H_
#define _CELL_SERVER_H_
#include"INetEvent.h"
#include"CellTask.h"
#include"ClientSocket.h"
#include
//网络消息发送任务
class CellSendMsgTask : public CellTask
{
public:
CellSendMsgTask(ClientSocket* pClient, DataHeader* pHead)
{
_pClient = pClient;
_pHeader = pHead;
}
//执行任务
virtual void DoTask()
{
_pClient->SendData(_pHeader);
delete _pHeader;
}
private:
ClientSocket* _pClient;
DataHeader* _pHeader;
};
//线程类
class CellServer
{
public:
//构造
CellServer(SOCKET sock = INVALID_SOCKET);
//析构
virtual ~CellServer();
//处理事件
void setEventObj(INetEvent* event);
//关闭socket
void CloseSocket();
//判断是否工作中
bool IsRun();
//查询是否有待处理消息
bool OnRun();
//接收数据
int RecvData(ClientSocket* t_client);//处理数据
//响应数据
void NetMsg(DataHeader* pHead, ClientSocket* pClient);
//增加客户端
void addClient(ClientSocket* client);
//启动线程
void Start();
//获取该线程内客户端数量
int GetClientCount() const;
//添加任务
void AddSendTask(ClientSocket* pClient, DataHeader* pHead);
private:
//select优化
SOCKET _maxSock;//最大socket值
fd_set _fd_read_bak;//读集合备份
bool _client_change;//客户端集合bool true表示发生改变 需重新统计 fd_read集合
//缓冲区相关
char* _Recv_buf;//接收缓冲区
//socket相关
SOCKET _sock;
//正式客户队列
std::vector<ClientSocket*> _clients;//储存客户端
//客户缓冲区
std::vector<ClientSocket*> _clientsBuf;
std::mutex _mutex;//锁
//线程
std::thread* _pThread;
//退出事件接口
INetEvent* _pNetEvent;
//发送线程队列
CellTaskServer _taskServer;
};
#endif
在服务端源码基本完成后,我开始为内存池的连接进行准备。我打算在VS2019上尝试动态库和静态库的生成与链接。内存池我打算首先用静态库来链接,在之后可能我会改为动态库链接。
在网上查阅资料后,我按如下步骤进行静态库生成与链接:
//连接静态库 此静态库里含有一个内存池 在该解决方案代码中不加也可以连接上
#pragma comment(lib, "guguAlloc.lib")
Alloctor.h
/*
* 本文件中重载了new/delete操作
* 使new/delete调用内存池
* 2021/4/22
*/
#ifndef _Alloctor_h_
#define _Alloctor_h_
void* operator new(size_t size);
void operator delete(void* p);
void* operator new[](size_t size);
void operator delete[](void* p);
void* mem_alloc(size_t size);
void mem_free(void* p);
#endif
MemoryAlloc.h
/*
内存池类
对内存块进行管理
2021/4/22
*/
#ifndef _Memory_Alloc_h_
#define _Memory_Alloc_h_
//导入内存块头文件
#include"MemoryBlock.h"
class MemoryAlloc
{
public:
MemoryAlloc();
virtual ~MemoryAlloc();
//设置初始化
void setInit(size_t nSize, size_t nBlockSize);
//初始化
void initMemory();
//申请内存
void* allocMem(size_t nSize);
//释放内存
void freeMem(void* p);
protected:
//内存池地址
char* _pBuf;
//头部内存单元
MemoryBlock* _pHeader;
//内存块大小
size_t _nSize;
//内存块数量
size_t _nBlockSize;
//多线程锁
std::mutex _mutex;
};
#endif
MemoryBlock.h
/*
内存块类
内存管理的最小单位
2021/4/22
*/
#ifndef _Memory_Block_h_
#define _Memory_Block_h_
//声明内存池类
class MemoryAlloc;
//最底层导入内存头文件/断言头文件/锁头文件
#include
#include
#include
//如果为debug模式则加入调试信息
#ifdef _DEBUG
#include
#define xPrintf(...) printf(__VA_ARGS__)
#else
#define xPrintf(...)
#endif
class MemoryBlock
{
public:
//内存块编号
int _nID;
//引用情况
int _nRef;
//所属内存池
MemoryAlloc* _pAlloc;
//下一块位置
MemoryBlock* _pNext;
//是否在内存池内
bool _bPool;
private:
};
#endif
MemoryMgr.h
/*
内存管理工具类
对内存池进行管理
2021/4/22
*/
#ifndef _Memory_Mgr_h_
#define _Memory_Mgr_h_
//内存池最大申请
#define MAX_MEMORY_SIZE 128
//导入内存池模板类
#include"MemoryAlloc.h"
class MemoryMgr
{
public:
//饿汉式单例模式
static MemoryMgr* Instance();
//申请内存
void* allocMem(size_t nSize);
//释放内存
void freeMem(void* p);
//增加内存块引用次数
void addRef(void* p);
private:
MemoryMgr();
virtual ~MemoryMgr();
//内存映射初始化
void init_szAlloc(int begin, int end, MemoryAlloc* pMem);
private:
//映射数组
MemoryAlloc* _szAlloc[MAX_MEMORY_SIZE + 1];
//64字节内存池
MemoryAlloc _mem64;
//128字节内存池
MemoryAlloc _mem128;
};
#endif
内存池静态库链接完成后,我开始准备新建动态库项目,存放自实现计时器类和报文命令类型。
在网上查阅资料后,我按如下步骤进行静态库生成与链接:
guguTimer.h/guguTimer.cpp
为计时器类声明/定义、CMD.h
为报文命令类型文件)__declspec(dllexport)
include"pch.h"
来保证预编译正常进行)guguDll.dll
、guguDll.lib
、pch.h
、framework.h
、guguTimer.h
、CMD.h
)//连接动态库 此动态库里含有计时器类timer 和 cmd命令
#include "pch.h"
#pragma comment(lib, "guguDll.lib")
pch.h
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
#include "guguTimer.h"
#include "CMD.h"
#endif //PCH_H
framework
#pragma once//懒得改了(
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include
guguTimer.h
/*
* 计时器类
* 2021/4/23
*/
#ifndef MY_TIMER_H_
#define MY_TIMER_H_
#include
class __declspec(dllexport) mytimer
{
private:
public:
mytimer();
virtual ~mytimer();
//调用update时,使起始时间等于当前时间
void UpDate();
//调用getsecond方法时,经过的时间为当前时间减去之前统计过的起始时间。
double GetSecond();
};
#endif
CMD.h
/*
* 报文数据类型
* 2021/4/23
*/
#ifndef _CMD_H_
#define _CMD_H_
//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_NEW_USER_JOIN,//新用户登入
CMD_ERROR//错误
};
//定义数据包头
struct DataHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DataHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DataHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DataHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DataHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};
//包5 新用户登入 传输通告
struct NewUserJoin : public DataHeader
{
NewUserJoin()//初始化包头
{
this->cmd = CMD_NEW_USER_JOIN;
this->date_length = sizeof(NewUserJoin);
}
char UserName[32];//用户名
};
#endif