C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)

网络编程学习记录

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows(本文项目全部使用windows平台下vs2019开发,故本文项目不支持linux平台)

笔记一:建立基础TCP服务端/客户端  点我跳转
笔记二:网络数据报文的收发  点我跳转
笔记三:升级为select网络模型  点我跳转
笔记四:跨平台支持Windows、Linux系统  点我跳转
笔记五:源码的封装  点我跳转
笔记六:缓冲区溢出与粘包分包  点我跳转
笔记七:服务端多线程分离业务处理高负载  点我跳转
笔记八:对socket select网络模型的优化  点我跳转
笔记九:消息接收与发送分离  点我跳转
笔记十:项目化 (加入内存池静态库 / 报文动态库)  


笔记十

  • 网络编程学习记录
  • 一、思路与准备
  • 二、服务端本体 项目
    • 1. 思路
    • 2. 头文件源码
      • ①服务端基础接口部分
      • ②服务端主线程部分
      • ③客户端类部分
      • ④子线程类任务处理(发送)部分
      • ⑤子线程类接收部分
  • 三、内存池静态库 项目
    • 1. 思路
    • 2. 头文件源码
      • ①重载new/delete部分
      • ②内存池类部分
      • ③内存块类部分
      • ④内存管理工具类
  • 四、计时/报文动态库 项目
    • 1. 思路
    • 2. 头文件源码
      • ①`pch.h`
      • ②`framework`
      • ③`guguTimer.h`
      • ④`CMD.h`

一、思路与准备

  之前的客户端虽然可以跑起来,但是声明和实现全写于一个hpp文件中,随着代码日渐增多,增删改变得越发困难。所以我决定尝试将其实现的更加标准。本次我准备的内容如下:

  • 服务端源码 —— C++网络编程学习:消息接收与发送分离(即笔记九版本的源码)
  • 内存池源码 —— C++学习记录:内存池设计与实现 及其详细代码
  • 计时器类 —— C++学习记录:基于chrono库的高精度计时器
  • 报文CMD文件 —— 即报文类型定义,在之前的网络编程笔记中可以找到

  我首先准备将之前服务端源代码的hpp文件进行分离,分离成单独类声明与实现。且服务端本体源码放在一个项目中。随后新建静态库项目存储内存池源码,使服务端源码项目链接内存池静态库。最后新建一个动态库项目,里面存放计时器类代码和报文CMD文件,因为服务端和客户端程序都要用这个库里的文件,为了今后方便改动,我选择使用动态库。

二、服务端本体 项目

1. 思路

  首先,服务端源码按功能可分为五个部分:

  1. 服务端基础接口部分。此部分定义了几个服务端的基本操作,可以通过继承重写这几个基本操作实现不同的功能。
  2. 服务端主线程部分。此部分调用基础的socket函数,与客户端建立socket连接,仅监控是否有新客户端加入。
  3. 客户端类部分。每当有新客户端加入,都会新建一个客户端对象,通过该客户端对象(获取socket/使用缓冲区)发送网络报文。
  4. 子线程类任务处理(发送)部分。此类每一个对象即为一条新的子线程,用来处理服务端与客户端之间的网络报文发送任务。
    任务处理接口。通过重写该接口,实现自己的任务处理方式。
  5. 子线程类接收部分。此类每一个对象即为一条新的子线程,用来处理监控服务端与客户端之间的网络报文接收。
    重写任务处理接口方法。实现自己的任务处理方式。

  按照五个部分的关系等,可画出如下的关系图:

  由此,我令 ⑤部分 include ①③④部分,再令 ②部分 include ⑤部分,即可实现项目各文件间的关联。

2. 头文件源码

①服务端基础接口部分

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

三、内存池静态库 项目

1. 思路

  在服务端源码基本完成后,我开始为内存池的连接进行准备。我打算在VS2019上尝试动态库和静态库的生成与链接。内存池我打算首先用静态库来链接,在之后可能我会改为动态库链接。
  在网上查阅资料后,我按如下步骤进行静态库生成与链接:

  1. 更改该项目配置类型为静态库类型
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第1张图片
  2. 关闭预编译头(我是关掉了,也可以把内存池放在预编译头中)
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第2张图片
  3. 在解决方案属性中,使得应用程序项目(即服务端项目)依赖于内存池静态库项目。这样在编译服务端项目时,会自动编译更新静态库
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第3张图片
  4. 在应用程序项目(即服务端项目)属性中,在链接器选项中的附加依赖项中,添加上lib静态库文件
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第4张图片
  5. 由于两个项目(服务端和静态库)在同一个解决方案,所以可以不用用代码链接静态库(我个人实验得出结论,不一定对)
//连接静态库 此静态库里含有一个内存池 在该解决方案代码中不加也可以连接上
#pragma comment(lib, "guguAlloc.lib")

C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第5张图片

2. 头文件源码

①重载new/delete部分

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

四、计时/报文动态库 项目

1. 思路

  内存池静态库链接完成后,我开始准备新建动态库项目,存放自实现计时器类报文命令类型
  在网上查阅资料后,我按如下步骤进行静态库生成与链接:

  1. 新建动态库项目,配置类型为动态库
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第6张图片
  2. 此项目中,我使用了预编译头文件
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第7张图片
  3. 添加自实现计时器类报文命令类型文件
    (下图中guguTimer.h/guguTimer.cpp为计时器类声明/定义、CMD.h为报文命令类型文件)
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第8张图片
  4. 添加库导出关键字__declspec(dllexport)
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第9张图片
  5. 将计时类的成员变量改为全局变量,保证生命周期
    (cpp文件需要include"pch.h"来保证预编译正常进行)
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第10张图片
  6. 编译动态库,得到dll文件和lib文件
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第11张图片
  7. dll文件lib文件、动态库中所有的头文件复制到服务端项目的源码文件夹下
    (如下图所示,复制的文件有:guguDll.dllguguDll.libpch.hframework.hguguTimer.hCMD.h)
    C++网络编程学习:项目化 (加入内存池静态库 / 报文动态库)_第12张图片
  8. 链接动态库、include预编译文件,此时动态库链接完成
//连接动态库 此动态库里含有计时器类timer 和 cmd命令
#include "pch.h"
#pragma comment(lib, "guguDll.lib")

2. 头文件源码

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

你可能感兴趣的:(C/C++,网络编程,网络,c++,内存池,动态库和静态库,C++网络编程)