網路編程學習記錄
- 使用的語言為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`
- 五、專案完整原始碼(github)
一、思路與準備
??之前的客戶端雖然可以跑起來,但是宣告和實作全寫于一個hpp檔案中,隨著代碼日漸增多,增刪改變得越發困難,所以我決定嘗試將其實作的更加標準,本次我準備的內容如下:
- 服務端原始碼 —— C++網路編程學習:訊息接收與發送分離(即筆記九版本的原始碼)
- 記憶體池原始碼 —— C++學習記錄:記憶體池設計與實作 及其詳細代碼
- 計時器類 —— C++學習記錄:基于chrono庫的高精度計時器
- 報文CMD檔案 —— 即報文型別定義,在之前的網路編程筆記中可以找到
??我首先準備將之前服務端源代碼的hpp檔案進行分離,分離成單獨類宣告與實作,且服務端本體原始碼放在一個專案中,隨后新建靜態庫專案存盤記憶體池原始碼,使服務端原始碼專案鏈接記憶體池靜態庫,最后新建一個動態庫專案,里面存放計時器類代碼和報文CMD檔案,因為服務端和客戶端程式都要用這個庫里的檔案,為了今后方便改動,我選擇使用動態庫,
二、服務端本體 專案
1. 思路
??首先,服務端原始碼按功能可分為五個部分:
- 服務端基礎介面部分,此部分定義了幾個服務端的基本操作,可以通過繼承重寫這幾個基本操作實作不同的功能,
- 服務端主執行緒部分,此部分呼叫基礎的socket函式,與客戶端建立socket連接,僅監控是否有新客戶端加入,
- 客戶端類部分,每當有新客戶端加入,都會新建一個客戶端物件,通過該客戶端物件(獲取socket/使用緩沖區)發送網路報文,
- 子執行緒類任務處理(發送)部分,此類每一個物件即為一條新的子執行緒,用來處理服務端與客戶端之間的網路報文發送任務,
任務處理介面,通過重寫該介面,實作自己的任務處理方式, - 子執行緒類接收部分,此類每一個物件即為一條新的子執行緒,用來處理監控服務端與客戶端之間的網路報文接收,
重寫任務處理介面方法,實作自己的任務處理方式,
??按照五個部分的關系等,可畫出如下的關系圖:

??由此,我令 ⑤部分 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<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
/*
* 客戶端類
* 服務端物件中每加入一個新的客戶端,都會新建一個客戶端物件
* 通過該客戶端物件向客戶端進行發送訊息等操作
*
* 目前來說只實作了定量發送資料,即發送緩沖區滿后發送訊息,下一步預備完善為定時定量發送資訊
* 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
/*
* 子執行緒發送部分
* 本頭檔案中實作了任務執行的分離操作
* 通過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
/*
* 子執行緒接收部分
* 本檔案中實作執行緒類以及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上嘗試動態庫和靜態庫的生成與鏈接,記憶體池我打算首先用靜態庫來鏈接,在之后可能我會改為動態庫鏈接,
??在網上查閱資料后,我按如下步驟進行靜態庫生成與鏈接:
- 更改該專案配置型別為靜態庫型別

- 關閉預編譯頭(我是關掉了,也可以把記憶體池放在預編譯頭中)

- 在解決方案屬性中,使得應用程式專案(即服務端專案)依賴于記憶體池靜態庫專案,這樣在編譯服務端專案時,會自動編譯更新靜態庫

- 在應用程式專案(即服務端專案)屬性中,在聯結器選項中的附加依賴項中,添加上lib靜態庫檔案

- 由于兩個專案(服務端和靜態庫)在同一個解決方案,所以可以不用用代碼鏈接靜態庫(我個人實驗得出結論,不一定對)
//連接靜態庫 此靜態庫里含有一個記憶體池 在該解決方案代碼中不加也可以連接上
#pragma comment(lib, "guguAlloc.lib")

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<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
/*
記憶體管理工具類
對記憶體池進行管理
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. 思路
??記憶體池靜態庫鏈接完成后,我開始準備新建動態庫專案,存放自實作計時器類和報文命令型別,
??在網上查閱資料后,我按如下步驟進行靜態庫生成與鏈接:
- 新建動態庫專案,配置型別為動態庫

- 此專案中,我使用了預編譯頭檔案

- 添加自實作計時器類和報文命令型別檔案
(下圖中guguTimer.h/guguTimer.cpp為計時器類宣告/定義、CMD.h為報文命令型別檔案)

- 添加庫匯出關鍵字
__declspec(dllexport)

- 將計時類的成員變數改為全域變數,保證生命周期
(cpp檔案需要include"pch.h"來保證預編譯正常進行)

- 編譯動態庫,得到dll檔案和lib檔案

- 將dll檔案、lib檔案、動態庫中所有的頭檔案復制到服務端專案的原始碼檔案夾下
(如下圖所示,復制的檔案有:guguDll.dll、guguDll.lib、pch.h、framework.h、guguTimer.h、CMD.h)

- 鏈接動態庫、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 <windows.h>
③guguTimer.h
/*
* 計時器類
* 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
/*
* 報文資料型別
* 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為動態庫

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/280579.html
標籤:其他
上一篇:圖之鄰接表詳解(C語言版)
下一篇:Java——資料結構之順序表
