主頁 > 軟體設計 > C++網路編程學習:服務端多執行緒分離業務處理高負載

C++網路編程學習:服務端多執行緒分離業務處理高負載

2021-02-08 13:30:43 軟體設計

網路編程學習記錄

  • 使用的語言為C/C++
  • 原始碼支持的平臺為:Windows / Linux

筆記一:建立基礎TCP服務端/客戶端 ?點我跳轉
筆記二:網路資料報文的收發 ?點我跳轉
筆記三:升級為select網路模型 ?點我跳轉
筆記四:跨平臺支持Windows、Linux系統 ?點我跳轉
筆記五:原始碼的封裝 ?點我跳轉
筆記六:緩沖區溢位與粘包分包 ?點我跳轉
筆記七:服務端多執行緒分離業務處理高負載 ?點我跳轉


筆記七

  • 網路編程學習記錄
  • 一、思路與準備
  • 二、代碼的改進
    • 1.新建子執行緒類
    • 2.客戶端主執行緒類的更改
    • 3.引入介面,實作子執行緒向主執行緒通信
  • 三、詳細代碼實作
    • 1.計時器頭檔案 `mytimer.hpp`
    • 2.命令頭檔案 `CMD.h`
    • 3.服務端頭檔案 `TcpServer.hpp`
    • 4.服務端樣例代碼 `server.cpp`

一、思路與準備

之前的服務端思路大概是如下的:

1.建立socket
2.系結埠IP
3.監聽埠
while(true)
{
	4.使用select函式獲取存在待監聽事件的socket
	5.如果有新的連接則與新的客戶端連接
	6.如果有待監聽事件,則對其進行處理(接受與發送)
}
7.關閉socket

??但是,這樣的架構在select處理事件較多時,很容易效率低下,對于這類問題,我們可以引入生產者與消費者模式,來處理此類并發問題,
??主執行緒為生產者執行緒,用來處理新客戶端加入事件,把新客戶端分配至消費者執行緒中,消費者執行緒便是我們建立的新執行緒,專門用來處理客戶端發送的報文,這樣就實作了事件處理的分離,從而使服務端處理效率更高,當過多客戶端同時涌入時,可以更快的建立連接(因為有專門的執行緒用來處理這一事件);而當客戶端發送報文頻率很快時,多執行緒處理報文也會大大提高效率,

  • 大致改進思路如下,紅色的為此次需要加入的核心,黑色為原本架構
    核心思路圖

??所以我們首先需要新建一個執行緒類,用來封裝關于消費者執行緒的內容,從而建立多執行緒架構,隨后,在本次的改進中,我決定加入計時器用來統計資料以及顯示資料,主要需要統計的資料為:當前客戶端連接數量、資料包的每秒接收數量,同時,我也對報文型別進行了分離,把報文型別放在單獨的頭檔案里,這樣既方便更改也方便參考,

  • 1.計時器相關請點這里 ?? 2.多執行緒相關請點這里

二、代碼的改進

1.新建子執行緒類

  • 首先是新建執行緒類CellServer,其中包含的基礎方法以及相關變數如下:
//執行緒類
class CellServer
{
public:
	//構造 
	CellServer(SOCKET sock = INVALID_SOCKET);
	//析構
	~CellServer();
	//關閉socket 
	void CloseSocket();
	//判斷是否作業中 
	bool IsRun();
	//查詢是否有待處理訊息 
	bool OnRun();
	//接收資料
	int RecvData(ClientSocket *t_client);
	//回應資料
	void NetMsg(DataHeader *head,SOCKET temp_socket);
	//增加客戶端 
	void addClient(ClientSocket* client);
	//啟動執行緒
	void Start(); 
	//獲取該執行緒內客戶端數量
	int GetClientCount()
	
private:
	//緩沖區相關 
	char *_Recv_buf;//接識訓沖區 
	//socket相關 
	SOCKET _sock; 
	//正式客戶佇列 
	std::vector<ClientSocket*> _clients;//儲存客戶端
	//客戶緩沖區佇列
	std::vector<ClientSocket*> _clientsBuf; 
	std::mutex _mutex;//鎖
	//執行緒 
	std::thread* _pThread;

public:
	std::atomic_int _recvCount;//接收包的數量 
};

  • 大致處理思路如下:
執行緒外:
Start() 首先呼叫該方法啟動執行緒

新客戶端加入:
GetClientCount() 首先主執行緒使用這個方法獲取各個執行緒內客戶端數量
//這個添加客戶端的方法內涉及到臨界區,需要上鎖
addClient(ClientSocket* client) 主執行緒找到客戶端數量最少的執行緒,使用該執行緒添加客戶端至緩沖佇列

執行緒內:
OnRun()//運行執行緒
{
	while(IsRun())//判斷是否作業中
	{
		1.將緩沖佇列中的客戶資料加入正式佇列
		2.正式客戶佇列為空的話,continue本次回圈
		3.select選擇出待處理事件,錯誤的話就關閉所有連接CloseSocket()
		4.對待處理事件進行接收RecvData(),接收包的數量加一,隨后處理NetMsg()
	}
}

2.客戶端主執行緒類的更改

??由于我們處理事件都改為在子執行緒中,所以首先主執行緒中是不需要處理報文訊息了,所以類中接收訊息和處理訊息的方法都可以洗掉了,同時我們加入Start方法用來啟動子執行緒,加入time4msg方法用來顯示子執行緒中的客戶端數量、每秒收包數等資料,

  • 主執行緒類TcpServer,更改后如下:
class TcpServer : INetEvent
{
public:
	//構造 
	TcpServer();
	//析構 
	~TcpServer();
	//初始化socket 回傳1為正常 
	int InitSocket();
	//系結IP/埠
	int Bind(const char* ip,unsigned short port);
	//監聽埠
	int Listen(int n);
	//接受連接
	int Accept();
	//添加客戶端至服務端  
	void AddClientToServer(ClientSocket* pClient);
	//執行緒啟動 
	void Start();
	//關閉socket 
	void CloseSocket();
	//判斷是否作業中 
	bool IsRun();
	//查詢是否有待處理訊息 
	bool OnRun();
	//顯示各執行緒資料資訊 
	void time4msg();
	
private:
	//socket相關 
	SOCKET _sock; 
	std::vector<ClientSocket*> _clients;//儲存客戶端
	std::vector<CellServer*> _cellServers;//子執行緒們 
	//計時器
	mytimer _time; 
};

  • 大致處理思路如下:計時器相關請點這里
呼叫TcpServer封裝類建立服務端的流程:
1.InitSocket() 建立一個socket
2.Bind(const char* ip,unsigned short port) 系結埠和IP
3.Listen(int n) 監聽
4.Start() 執行緒啟動
while(5.IsRun()) 主執行緒回圈 
{
	6.OnRun() 開始select選擇處理事件
}
7.CloseSocket() 關閉socket

主執行緒內:
OnRun()
{
	time4msg()顯示資料 
	select選擇出新客戶端加入事件
	如果有新客戶端加入,呼叫Accept()接受連接
	Accept()連接成功后,呼叫AddClientToServer(ClientSocket* pClient)分配客戶端到子執行緒中
}

AddClientToServer()內:
首先呼叫子執行緒的GetClientCount()方法獲取各條子執行緒中的客戶端數量
隨后呼叫子執行緒的addClient(ClientSocket* client)方法,把新客戶端添加至客戶端最少的執行緒中

time4msg()內:
首先GetSecond()獲取計時器的計時
如果大于一秒,就統計客戶端的情況:子執行緒內_recvCount為收包數,主執行緒內_clients.size()獲取客戶端數量
顯示后UpDate()重置計時器,并且重置收包數,從而達到統計每秒收包數的作用

3.引入介面,實作子執行緒向主執行緒通信

??通過前兩步的實作,多執行緒服務端已經初步完成,接下來需要進行一些完善,
??我們很容易可以發現,子執行緒物件是在主執行緒Start()方法被創建的,隨后被加入容器_cellServers儲存,這就導致主執行緒中可以呼叫子執行緒類中的方法與成員變數,但是子執行緒中卻無法呼叫主執行緒的方法與成員變數,從而導致當子執行緒中有客戶端退出時,主執行緒無法了解,
??對于這種情況,我們可以創建一個介面,讓主執行緒類繼承這個介面,子執行緒即可通過這個介面呼叫主執行緒中的特定方法,

  • 介面類INetEvent如下:
class INetEvent
{
public:
	//有客戶端退出 
	virtual void OnLeave(ClientSocket* pClient) = 0;
private:	
};
  • 主執行緒類與子執行緒類中的相關實作:
1.首先是主執行緒類繼承該介面:
class TcpServer : INetEvent

2.隨后實作介面中的虛方法:
//客戶端退出
void OnLeave(ClientSocket* pClient)
{
	//找到退出的客戶端 
	for(int n=0; n<_clients.size(); n++)
	{
		if(_clients[n] == pClient)
		{
			auto iter = _clients.begin() + n;
			if(iter != _clients.end())
			{
				_clients.erase(iter);//移除 
			}
		}
	} 
} 
即可實作呼叫該方法,移除客戶端容器中指定客戶端

3.隨后在子執行緒類中添加私有成員變數: 
private:
	INetEvent* _pNetEvent; 
創建介面物件

4.創建方法,讓介面物件指向主執行緒類
void setEventObj(INetEvent* event)
{
	_pNetEvent = event; 
}
event傳進去主執行緒即可,介面物件即指向主執行緒

5.主執行緒創建、啟動子執行緒類時,呼叫該方法,傳入自身this
子執行緒物件->setEventObj(this);

6.隨后即可通過子執行緒呼叫主執行緒的OnLeave()方法洗掉客戶端
_pNetEvent->OnLeave(要洗掉的客戶端);

三、詳細代碼實作

1.計時器頭檔案 mytimer.hpp

#ifndef MY_TIMER_H_
#define MY_TIMER_H_

#include<chrono>

class mytimer
{
private:
    std::chrono::steady_clock::time_point _begin;//起始時間
    std::chrono::steady_clock::time_point _end;//終止時間
public:
    mytimer()
	{
		_begin = std::chrono::steady_clock::time_point();
		_end = std::chrono::steady_clock::time_point();
	}
    
	virtual ~mytimer(){};  
    
    //呼叫update時,使起始時間等于當前時間
    void UpDate()
    {
        _begin = std::chrono::steady_clock::now();
    }

	//呼叫getsecond方法時,經過的時間為當前時間減去之前統計過的起始時間,
    double GetSecond()
    {
        _end = std::chrono::steady_clock::now();
        //使用duration型別變數進行時間的儲存   duration_cast是型別轉換方法
        std::chrono::duration<double> temp = std::chrono::duration_cast<std::chrono::duration<double>>(_end - _begin);
       	return temp.count();//count() 獲取當前時間的計數數量
    }
    
};

#endif

2.命令頭檔案 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];//用戶名 
};

3.服務端頭檔案 TcpServer.hpp

#ifndef _TcpServer_hpp_
#define _TcpServer_hpp_

#ifdef _WIN32
	#define FD_SETSIZE 10240 
	#define WIN32_LEAN_AND_MEAN
	#include<winSock2.h>
	#include<windows.h>
	#pragma comment(lib,"ws2_32.lib")//鏈接此元件 windows特有 
#else
	#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

#include"CMD.h"//命令 
#include"mytimer.hpp"//計時器 
#include<stdio.h>
#include<vector>
#include<thread> 
#include<mutex>
#include<atomic>

//緩沖區大小 
#ifndef RECV_BUFFER_SIZE
	#define RECV_BUFFER_SIZE 4096
#endif 

//執行緒數量 
#define _THREAD_COUNT 4

//客戶端類 
class ClientSocket
{
public:
	//構造 
	ClientSocket(SOCKET sockfd = INVALID_SOCKET)
	{
		_sockfd = sockfd;
		//緩沖區相關 
		_Msg_buf = new char[RECV_BUFFER_SIZE*10];
		_Len_buf = 0; 
	}
	//析構 
	virtual ~ClientSocket()
	{
		delete[] _Msg_buf;
	}
	
	//獲取socket 
	SOCKET GetSockfd()
	{
		return _sockfd;
	}
	
	//獲取緩沖區 
	char* MsgBuf()
	{
		return _Msg_buf;
	} 
	
	//獲取緩沖區尾部變數 
	int GetLen()
	{
		return _Len_buf;	
	} 
	
	//設定緩沖區尾巴變數
	void SetLen(int len)
	{
		_Len_buf = len;
	} 
		
private:	
	SOCKET _sockfd;
	//緩沖區相關 
	char *_Msg_buf;//訊息緩沖區 
	int _Len_buf;//緩沖區資料尾部變數
}; 

//事件介面
class INetEvent
{
public:
	//有客戶端退出 
	virtual void OnLeave(ClientSocket* pClient) = 0;
private:	
};

//執行緒類
class CellServer
{
public:
	//構造 
	CellServer(SOCKET sock = INVALID_SOCKET)
	{
		_sock = sock; 
		_pThread = nullptr;
		_pNetEvent = nullptr;
		_recvCount = 0; 
		//緩沖區相關 
		_Recv_buf = new char[RECV_BUFFER_SIZE];
	}
	//析構
	~CellServer()
	{
		delete[] _Recv_buf;
		//關閉socket 
		CloseSocket();
		_sock = INVALID_SOCKET;
	} 
	
	//處理事件 
	void setEventObj(INetEvent* event)
	{
		_pNetEvent = event; 
	}
	
	//關閉socket 
	void CloseSocket()
	{
		if(INVALID_SOCKET != _sock) 
		{
#ifdef _WIN32
			//關閉客戶端socket
			for(int n=0; n<_clients.size(); ++n)
			{
				closesocket(_clients[n]->GetSockfd());
				delete _clients[n];
			}
			//關閉socket
			closesocket(_sock); 
			//清除windows socket 環境 
			WSACleanup(); 
#else
			//關閉客戶端socket
			for(int n=0; n<_clients.size(); ++n)
			{
				close(_clients[n]->GetSockfd());
				delete _clients[n];
			}
			//關閉socket/LINUX
			close(_sock);
#endif
			_sock = INVALID_SOCKET;
			_clients.clear();
		}
	} 
	
	
	//判斷是否作業中 
	bool IsRun()
	{
		return _sock != INVALID_SOCKET; 
	}
	
	//查詢是否有待處理訊息 
	bool OnRun()
	{
		while(IsRun())
		{
			//將緩沖佇列中的客戶資料加入正式佇列 
			if(_clientsBuf.size() > 0)
			{
				std::lock_guard<std::mutex> lock(_mutex);//上鎖 
				for(auto client :_clientsBuf)
				{
					_clients.push_back(client);
				}
				_clientsBuf.clear();
			} 
			//如果沒有需要處理的客戶端就跳過 
			if(_clients.empty())
			{
				std::chrono::milliseconds t(1);//休眠一毫秒 
				std::this_thread::sleep_for(t);
				continue;
			}
			fd_set fdRead;//建立集合 
			FD_ZERO(&fdRead);//清空集合 
			SOCKET maxSock = _clients[0]->GetSockfd();//最大socket 
			//把連接的客戶端 放入read集合 
			for(int n=_clients.size()-1; n>=0; --n)
			{
				FD_SET(_clients[n]->GetSockfd(),&fdRead);
				if(maxSock < _clients[n]->GetSockfd())
				{
					maxSock = _clients[n]->GetSockfd();
				}
			}
			//select函式篩選select 
			int ret = select(maxSock+1,&fdRead,0,0,0); 
			if(ret<0)
			{
				printf("select任務結束\n");
				CloseSocket();
				return false;
			}
			//遍歷所有socket 查看是否有待處理事件 
			for(int n=0; n<_clients.size(); ++n)
			{
				if(FD_ISSET(_clients[n]->GetSockfd(),&fdRead))
				{
					if(-1 == RecvData(_clients[n]))//處理請求 客戶端退出的話 
					{
						std::vector<ClientSocket*>::iterator iter = _clients.begin()+n;//找到退出客戶端的地址
						if(iter != _clients.end())//如果是合理值
						{
							if(_pNetEvent)//主執行緒中洗掉客戶端 
							{
								_pNetEvent->OnLeave(_clients[n]);
							}
							delete _clients[n];
							_clients.erase(iter);//移除
						}
					}
				}
			}
			//printf("空閑時間處理其他業務\n");
		}
	} 
	
	//接收資料
	int RecvData(ClientSocket *t_client)//處理資料 
	{ 
		_recvCount++;//收包數量加一 
		//接收客戶端發送的資料  
		int buf_len = recv(t_client->GetSockfd(), _Recv_buf, RECV_BUFFER_SIZE, 0);
		if(buf_len<=0)
		{
			printf("客戶端已退出\n");
			return -1;
		} 
		
		//將接識訓沖區的資料拷貝到訊息緩沖區 
		memcpy(t_client->MsgBuf() + t_client->GetLen(), _Recv_buf, buf_len); 
		//訊息緩沖區的資料末尾后移 
		t_client->SetLen(t_client->GetLen() + buf_len);
		//判斷訊息緩沖區的資料長度是否大于等于包頭長度 
		while(t_client->GetLen() >= sizeof(DataHeader))//處理粘包問題 
		{
			//選出包頭資料 
			DataHeader* header = (DataHeader*)t_client->MsgBuf(); 
			//判斷訊息緩沖區內資料長度是否大于等于報文長度 避免少包問題 
			if(t_client->GetLen() >= header->date_length)
			{
				//計算出訊息緩沖區內剩余未處理資料的長度
				int size = t_client->GetLen() - header->date_length; 
				//回應資料 
				NetMsg(header,t_client->GetSockfd());
				//將訊息緩沖區剩余未處理的資料前移
				memcpy(t_client->MsgBuf(), t_client->MsgBuf() + header->date_length, size);
				//訊息緩沖區的資料末尾前移
				t_client->SetLen(size); 
			} 
			else
			{
				//訊息緩沖區資料不足 
				break; 
			} 
		}  
		return 0;
	}
	
	//回應資料
	void NetMsg(DataHeader *head,SOCKET temp_socket)
	{
		
	 	//printf("接收到包頭,命令:%d,資料長度:%d\n",head->cmd,head->date_length);
		switch(head->cmd)
		{
			case CMD_LOGIN://登錄 接收登錄包體 
			{
				Login *login = (Login*)head;
				/*
				進行判斷操作 
				*/
				//printf("%s已登錄\n密碼:%s\n",login->UserName,login->PassWord); 
				LoginResult *result = new LoginResult;	
				result->Result = 1;
				//SendData(result,temp_socket);
			}
			break;
			case CMD_LOGOUT://登出 接收登出包體 
			{
				Logout *logout = (Logout*)head;
				/*
				進行判斷操作 
				*/
				//printf("%s已登出\n",logout->UserName); 
				LogoutResult *result = new LogoutResult();
				result->Result = 1;
				//SendData(result,temp_socket);
			}
			break;
			default://錯誤 
			{
				head->cmd = CMD_ERROR; 
				head->date_length = 0; 
				//SendData(head,temp_socket); 
			}
			break;
		}
	}
	
	//增加客戶端 
	void addClient(ClientSocket* client)
	{
		std::lock_guard<std::mutex> lock(_mutex);
		//_mutex.lock();
		_clientsBuf.push_back(client);	
		//_mutex.unlock();
	} 
	
	//啟動執行緒
	void Start()
	{
		_pThread = new std::thread(std::mem_fun(&CellServer::OnRun),this);
		 
	} 
	
	//獲取該執行緒內客戶端數量
	int GetClientCount()
	{
		return _clients.size() + _clientsBuf.size();	
	} 
	
private:
	//緩沖區相關 
	char *_Recv_buf;//接識訓沖區 
	//socket相關 
	SOCKET _sock; 
	//正式客戶佇列 
	std::vector<ClientSocket*> _clients;//儲存客戶端
	//客戶緩沖區
	std::vector<ClientSocket*> _clientsBuf; 
	std::mutex _mutex;//鎖
	//執行緒 
	std::thread* _pThread;
	//退出事件介面 
	INetEvent* _pNetEvent; 

public:
	std::atomic_int _recvCount;//接收包的數量 
};

//服務端類 
class TcpServer : INetEvent
{
public:
	//構造 
	TcpServer()
	{
		_sock = INVALID_SOCKET; 
	}
	
	//析構 
	virtual ~TcpServer()
	{
		//關閉socket 
		CloseSocket();
	}
	
	//初始化socket 回傳1為正常 
	int InitSocket()
	{
#ifdef _WIN32
		//啟動windows socket 2,x環境 
		WORD ver = MAKEWORD(2,2); 
		WSADATA dat;
		if(0 != WSAStartup(ver,&dat))
		{
			return -1;//-1為環境錯誤 
		}
#endif 
		//創建socket 
		if(INVALID_SOCKET != _sock)
		{
			printf("<Socket=%d>關閉連接\n",_sock); 
			CloseSocket();//如果之前有連接 就關閉連接 
		}
		_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); 
		if(INVALID_SOCKET == _sock)
		{   
			return 0;//0為socket創建錯誤 
		} 
		return 1;
	} 
	
	//系結IP/埠
	int Bind(const char* ip,unsigned short port)
	{
		//如果為無效套接字 則初始化 
		if(INVALID_SOCKET == _sock)
		{
			InitSocket(); 
		}
		//系結網路埠和IP地址 
		sockaddr_in _myaddr = {}; 
		_myaddr.sin_family = AF_INET;//IPV4
		_myaddr.sin_port = htons(port);//埠
#ifdef _WIN32
		if(ip)//ip為空則監聽所有網卡 
		{
			_myaddr.sin_addr.S_un.S_addr = inet_addr(ip);//IP
		} 
		else
		{
			_myaddr.sin_addr.S_un.S_addr = INADDR_ANY;//IP
		}
#else
		if(ip)//ip為空則監聽所有網卡
		{
			_myaddr.sin_addr.s_addr = inet_addr(ip);//IP 
		}
		else
		{
			_myaddr.sin_addr.s_addr = INADDR_ANY;//IP 
		}
#endif
		if(SOCKET_ERROR == bind(_sock,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (強制轉換)sockaddr結構體 結構體大小 
		{
			printf("系結失敗\n");
			return 0;
		}
		else
		{
			printf("系結成功\n系結埠為%d\n",port);
			return 1;
		}
	}
	
	//監聽埠
	int Listen(int n)
	{
		//如果為無效套接字 則提示 
		if(INVALID_SOCKET == _sock)
		{
			printf("請先初始化套接字并系結IP埠\n");
			return 0;	
		}
		//監聽網路埠
		if(SOCKET_ERROR == listen(_sock,n))//最大連接佇列 
		{
			printf("監聽失敗\n");
			return 0;
		}
		else
		{
			printf("監聽成功\n");
			return 1; 
		}
	}
	
	//接受連接
	int Accept()
	{
		//等待接收客戶端連接
		sockaddr_in clientAddr = {};//新建sockadd結構體接收客戶端資料 
		int addr_len = sizeof(sockaddr_in);//獲取sockadd結構體長度 
		SOCKET temp_socket = INVALID_SOCKET;//宣告客戶端套接字 
#ifdef _WIN32	
		temp_socket = accept(_sock,(sockaddr*)&clientAddr,&addr_len);//自身套接字 客戶端結構體 結構體大小 	
#else
		temp_socket = accept(_sock,(sockaddr*)&clientAddr,(socklen_t*)&addr_len);//自身套接字 客戶端結構體 結構體大小 
#endif
		if(INVALID_SOCKET == temp_socket)//接收失敗 
		{
			printf("<Socket=%d>錯誤,接受到無效客戶端SOCKET\n",temp_socket);
			return 0;
		}
		else
		{ 
			//printf("新客戶端加入 count: %d\nIP地址為:%s \n", _clients.size(), inet_ntoa(clientAddr.sin_addr));  
			//群發所有客戶端 通知新用戶登錄 
			//NewUserJoin *user_join = new NewUserJoin(); 
			//strcpy(user_join->UserName,inet_ntoa(clientAddr.sin_addr));
			//SendDataToAll(user_join);
			//將新的客戶端加入動態陣列
			AddClientToServer(new ClientSocket(temp_socket));
			return 1;
		} 
	} 
	
	//添加客戶端至服務端  
	void AddClientToServer(ClientSocket* pClient)
	{
		_clients.push_back(pClient);
		//找出客戶端最少的執行緒 然后加入 
		auto pMinServer = _cellServers[0];
		for(auto pCellServer : _cellServers)
		{
			if(pMinServer->GetClientCount() > pCellServer->GetClientCount())
			{
				pMinServer = pCellServer;
			}	
		}
		pMinServer->addClient(pClient);
	}
	
	//執行緒啟動 
	void Start()
	{
		for(int n=0; n<_THREAD_COUNT; n++)
		{
			//執行緒加入容器 
			auto ser = new CellServer(_sock); 
			_cellServers.push_back(ser);
			ser->setEventObj(this);
			ser->Start(); 
		}
	}
	
	//關閉socket 
	void CloseSocket()
	{
		if(INVALID_SOCKET != _sock) 
		{
#ifdef _WIN32
			//關閉客戶端socket
			for(int n=0; n<_clients.size(); ++n)
			{
				closesocket(_clients[n]->GetSockfd());
				delete _clients[n];
			}
			//關閉socket
			closesocket(_sock); 
			//清除windows socket 環境 
			WSACleanup(); 
#else
			//關閉客戶端socket
			for(int n=0; n<_clients.size(); ++n)
			{
				close(_clients[n]->GetSockfd());
				delete _clients[n];
			}
			//關閉socket/LINUX
			close(_sock);
#endif
			_sock = INVALID_SOCKET;
			_clients.clear();
		}
	} 
	
	
	//判斷是否作業中 
	bool IsRun()
	{
		return _sock != INVALID_SOCKET; 
	}
	
	//查詢是否有待處理訊息 
	bool OnRun()
	{
		if(IsRun())
		{
			time4msg();//查看各執行緒資料資訊 
			fd_set fdRead;//建立集合 
			//fd_set fdWrite;
			//fd_set fdExcept;
			FD_ZERO(&fdRead);//清空集合 
			//FD_ZERO(&fdWrite); 
			//FD_ZERO(&fdExcept); 
			FD_SET(_sock,&fdRead);//放入集合 
			//FD_SET(_sock,&fdWrite); 
			//FD_SET(_sock,&fdExcept);
			timeval s_t = {0,0};//select最大回應時間 
			
			//select函式篩選select 
			int ret = select(_sock+1,&fdRead,0,0,&s_t); 
			if(ret<0)
			{
				printf("select任務結束\n");
				CloseSocket();
				return false;
			}
			if(FD_ISSET(_sock,&fdRead))//獲取是否有新socket連接 
			{
				FD_CLR(_sock,&fdRead);//清理
				Accept();//連接 
			}
			return true;
		}
		return false;
	} 
	
	//顯示各執行緒資料資訊 
	void time4msg()
	{
		auto t1 = _time.GetSecond();
		if(1.0 <= t1)
		{
			int recvCount = 0;
			for(auto ser: _cellServers)
			{
				recvCount += ser->_recvCount;
				ser->_recvCount = 0; 
			}
			//時間間隔  本機socket連接序號  客戶端數量  每秒收包數 
			printf("time<%lf>,socket<%d>,clients<%d>,recvCount<%d>\n", t1, _sock, _clients.size(),(int)(recvCount/t1));
			_time.UpDate();
		}
	} 
	
	//發送資料 
	int SendData(DataHeader *head,SOCKET temp_socket)
	{
		if(IsRun() && head)
		{
			send(temp_socket,(const char*)head,head->date_length,0);
			return 1;
		}
		return 0;
	}
	
	//向所有人發送資料
	void SendDataToAll(DataHeader *head)
	{
		for(int n=0;n<_clients.size();++n)
		{
			SendData(head, _clients[n]->GetSockfd());	
		} 
	} 
	
	//客戶端退出
	void OnLeave(ClientSocket* pClient)
	{
		//找到退出的客戶端 
		for(int n=0; n<_clients.size(); n++)
		{
			if(_clients[n] == pClient)
			{
				auto iter = _clients.begin() + n;
				if(iter != _clients.end())
				{
					_clients.erase(iter);//移除 
				}
			}
		} 
	} 
	 
private:
	//socket相關 
	SOCKET _sock; 
	std::vector<ClientSocket*> _clients;//儲存客戶端
	std::vector<CellServer*> _cellServers;//執行緒處理 
	//計時器
	mytimer _time; 
};

#endif

4.服務端樣例代碼 server.cpp

#include"TcpServer.hpp"
 
int main() 
{
	printf("Welcome\n");
	
	//建立tcp物件 
	TcpServer *tcp1 = new TcpServer();
	//建立一個socket
	tcp1->InitSocket();
	//系結埠和IP
	tcp1->Bind(NULL,8888);
	//監聽
	tcp1->Listen(5);
	//執行緒啟動
	tcp1->Start(); 
	//回圈 
	while(tcp1->IsRun())
	{
		tcp1->OnRun();
	}
	//關閉
	tcp1->CloseSocket();
	 
	printf("任務結束,程式已退出"); 
	getchar(); 
	
	return 0;
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/257781.html

標籤:其他

上一篇:什么是 geobuf?

下一篇:Springboot2.x 集成 quartz 實作動態定時任務

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more