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

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

一、思路与准备

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

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

二、服务端本体 项目

1. 思路

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

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

  按照五个部分的关系等,可画出如下的关系图:
在这里插入图片描述
  由此,我令 ⑤部分 include ①③④部分,再令 ②部分 include ⑤部分,即可实现项目各文件间的关联。

2. 头文件源码

①服务端基础接口部分

INetEvent.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* 本文件中定义了服务端的基础接口
* 在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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* 服务端类
* 实现基础的socket连接操作
* 通过start方法生成子线程 监控收包
* 主线程仅进行客户端加入退出监控
* 2021/4/22
*/
#ifndef _TCP_SERVER_H_
#define _TCP_SERVER_H_

#include"CellServer.h"
#include<stdio.h>
#include<atomic>

//服务端类
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
* 客户端类
* 服务端对象中每加入一个新的客户端,都会新建一个客户端对象
* 通过该客户端对象向客户端进行发送消息等操作
*
* 目前来说只实现了定量发送数据,即发送缓冲区满后发送消息,下一步预备完善为定时定量发送信息
* 2021/4/22
*/
#ifndef _CLIENT_SOCKET_H_
#define _CLIENT_SOCKET_H_

//socket相关内容
#ifdef _WIN32
#define FD_SETSIZE 1024
#define WIN32_LEAN_AND_MEAN
#include<winSock2.h>
#include<WS2tcpip.h>
#include<windows.h>
#pragma comment(lib, "ws2_32.lib")//链接此动态链接库 windows特有
//连接动态库 此动态库里含有计时器类timer 和 cmd命令
#include "pch.h"
#pragma comment(lib, "guguDll.lib")
//连接静态库 此静态库里含有一个内存池
#pragma comment(lib, "guguAlloc.lib")
#else
#include<sys/socket.h>
#include<arpa/inet.h>//selcet
#include<unistd.h>//uni std
#include<string.h>

#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
* 子线程发送部分
* 本头文件中实现了任务执行的分离操作
* 通过list结构存储需要执行的任务
* start()启动线程进行任务处理
* 为防止出现冲突,所有临界操作均进行上锁,且首先使用缓冲区储存新任务
*
* 2021/4/22
*/
#ifndef _CELL_Task_hpp_
#define _CELL_Task_hpp_

#include<thread>
#include<mutex>
#include<list>
#include <functional>//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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* 子线程接收部分
* 本文件中实现线程类以及DoTask接口
* 使服务端线程分离 主线程接收连接 其余子线程处理消息
*
* 初步实现了发送任务的接口,使其调用客户端对象的SendData方法发送消息
* 目前采用的是select结构 未来可能尝试其他结构
* 2021/4/22
*/
#ifndef _CELL_SERVER_H_
#define _CELL_SERVER_H_

#include"INetEvent.h"
#include"CellTask.h"
#include"ClientSocket.h"

#include <vector>

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

2. 头文件源码

①重载new/delete部分

Alloctor.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 本文件中重载了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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
内存池类
对内存块进行管理
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
内存块类
内存管理的最小单位
2021/4/22
*/

#ifndef _Memory_Block_h_
#define _Memory_Block_h_

//声明内存池类
class MemoryAlloc;
//最底层导入内存头文件/断言头文件/锁头文件
#include<stdlib.h>
#include<assert.h>
#include<mutex>
//如果为debug模式则加入调试信息
#ifdef _DEBUG
#include<stdio.h>
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
内存管理工具类
对内存池进行管理
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. 新建动态库项目,配置类型为动态库
    在这里插入图片描述
  2. 此项目中,我使用了预编译头文件
    加粗样式
  3. 添加自实现计时器类报文命令类型文件
    (下图中guguTimer.h/guguTimer.cpp为计时器类声明/定义、CMD.h为报文命令类型文件)
    在这里插入图片描述
  4. 添加库导出关键字__declspec(dllexport)
    在这里插入图片描述
  5. 将计时类的成员变量改为全局变量,保证生命周期
    (cpp文件需要include"pch.h"来保证预编译正常进行)
    在这里插入图片描述
  6. 编译动态库,得到dll文件和lib文件
    在这里插入图片描述
  7. dll文件lib文件、动态库中所有的头文件复制到服务端项目的源码文件夹下
    (如下图所示,复制的文件有:guguDll.dllguguDll.libpch.hframework.hguguTimer.hCMD.h)
    在这里插入图片描述
  8. 链接动态库、include预编译文件,此时动态库链接完成
    1
    2
    3
    //连接动态库 此动态库里含有计时器类timer 和 cmd命令
    #include "pch.h"
    #pragma comment(lib, "guguDll.lib")

2. 头文件源码

pch.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。

#ifndef PCH_H
#define PCH_H

// 添加要在此处预编译的标头
#include "framework.h"
#include "guguTimer.h"
#include "CMD.h"

#endif //PCH_H

framework

1
2
3
4
5
6
#pragma once//懒得改了(

#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>

guguTimer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* 计时器类
* 2021/4/23
*/
#ifndef MY_TIMER_H_
#define MY_TIMER_H_

#include<chrono>

class __declspec(dllexport) mytimer
{
private:

public:
mytimer();

virtual ~mytimer();

//调用update时,使起始时间等于当前时间
void UpDate();

//调用getsecond方法时,经过的时间为当前时间减去之前统计过的起始时间。
double GetSecond();

};

#endif

CMD.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*
* 报文数据类型
* 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

五、项目完整源码(github)

github链接
如下图:guguServer为服务端程序、guguAlloc为内存池静态库、guguDll为动态库
在这里插入图片描述