项目(百万并发网络通信架构)10.1---服务端select的性能瓶颈与优化

一、测试场景

  • 本文测试1000个客户端连接服务端,客户端只发送数据,服务端只接收数据,一方发送一方接收

二、客户端的代码如下

EasyTcpClient.hpp

  • EasyTcpClient.hpp代码没有变化,如下所示
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_

#ifdef _WIN32
	#define WIN32_LEAN_AND_MEAN
	#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
	#define _CRT_SECURE_NO_WARNINGS
	#include <windows.h>
	#include <WinSock2.h>
	#pragma comment(lib, "ws2_32.lib")
#else
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>
	//在Unix下没有这些宏,为了兼容,自己定义
	#define SOCKET int
	#define INVALID_SOCKET  (SOCKET)(~0)
	#define SOCKET_ERROR            (-1)
#endif

//接收缓冲区的大小
#ifndef RECV_BUFF_SIZE
#define RECV_BUFF_SIZE 10240
#endif // !RECV_BUFF_SIZE


#include <iostream>
#include <string.h>
#include <stdio.h>
#include "MessageHeader.hpp"

using namespace std;

class EasyTcpClient
{
public:
	EasyTcpClient() :_sock(INVALID_SOCKET), _isConnect(false), _lastPos(0) {
		memset(_recvBuff, 0, sizeof(_recvBuff));
		memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));
	}
	virtual ~EasyTcpClient() { CloseSocket(); }
public:
	void InitSocket();  //初始化socket
	void CloseSocket(); //关闭socket
	bool Onrun();       //处理网络消息
	bool isRun() { return ((_sock != INVALID_SOCKET) && _isConnect); }       //判断当前客户端是否在运行
	int  ConnectServer(const char* ip, unsigned int port); //连接服务器
	//使用RecvData接收任何类型的数据,然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息
	int  RecvData();                                //接收数据
	virtual void OnNetMessage(DataHeader* header);  //响应网络消息
	int  SendData(DataHeader* header, int nLen)   ; //发送数据
private:
	SOCKET _sock;
	bool   _isConnect;                       //当前是否连接
	char   _recvBuff[RECV_BUFF_SIZE];        //第一缓冲区(接收缓冲区),用来存储从网络缓冲区中接收的数据
	char   _recvMsgBuff[RECV_BUFF_SIZE * 5]; //第二缓冲区(消息缓冲区),将第一缓冲区中的数据存储在这个缓冲区中,并在这个缓冲区中对数据进行处理(粘包拆包处理)
	int    _lastPos;                         //用来标识当前消息缓冲区中数据的结尾位置
};

void EasyTcpClient::InitSocket()
{
	//如果之前有连接了,关闭旧连接,开启新连接
	if (isRun())
	{
		std::cout << "<Socket=" << (int)_sock << ">:关闭旧连接,建立了新连接" << std::endl;
		CloseSocket();
	}

#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif

	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		//std::cout << "<Socket=" << (int)_sock << ">:建立socket成功!" << std::endl;
	}
}

int EasyTcpClient::ConnectServer(const char* ip, unsigned int port)
{
	if (!isRun())
	{
		InitSocket();
	}

	//声明要连接的服务端地址(注意,不同平台的服务端IP地址也不同)
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
	_sin.sin_addr.s_addr = inet_addr(ip);
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(port);

	//连接服务端
	int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")失败!" << std::endl;
	}
	else {
		_isConnect = true;
		//std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")成功!" << std::endl;
	}
	
	return ret;
}

void EasyTcpClient::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		closesocket(_sock);
		WSACleanup();
#else
		close(_sock);
#endif
		_sock = INVALID_SOCKET;
		_isConnect = false;
	}
}

bool EasyTcpClient::Onrun()
{
	if (isRun())
	{
		fd_set fdRead;
		FD_ZERO(&fdRead);
		FD_SET(_sock, &fdRead);

		struct timeval t = { 0,0 };
		int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);
		if (ret < 0)
		{
			std::cout << "<Socket=" << _sock << ">:select出错!" << std::endl;
			return false;
		}
		if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据
		{
			FD_CLR(_sock, &fdRead);
			if (-1 == RecvData())
			{
				std::cout << "<Socket=" << _sock << ">:数据接收失败,或服务端已断开!" << std::endl;
				CloseSocket();
				return false;
			}
		}
		return true;
	}
	return false;
}

int EasyTcpClient::RecvData()
{
	int _nLen = recv(_sock, _recvBuff, RECV_BUFF_SIZE, 0);
	if (_nLen < 0) {
		std::cout << "<Socket=" << _sock << ">:recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << "<Socket=" << _sock << ">:接收数据失败,服务端已关闭!" << std::endl;
		return -1;
	}
	//std::cout << "_nLen=" << _nLen << std::endl;
	
	//将获取的数据拷贝到消息缓冲区
	memcpy(_recvMsgBuff + _lastPos, _recvBuff, _nLen);
	_lastPos += _nLen;

	//如果_recvMsgBuff中的数据长度大于等于DataHeader
	while (_lastPos >= sizeof(DataHeader))
	{
		DataHeader* header = (DataHeader*)_recvMsgBuff;
		//如果_lastPos的位置大于等于一个数据包的长度,那么就会这个数据包进行处理
		if (_lastPos >= header->dataLength)
		{
			//剩余未处理消息缓冲区的长度
			int nSize = _lastPos - header->dataLength;
			//处理网络消息
			OnNetMessage(header);
			//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移
			memcpy(_recvMsgBuff, _recvMsgBuff + header->dataLength, nSize);
			_lastPos = nSize;
		}
		else {
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

void EasyTcpClient::OnNetMessage(DataHeader* header)
{
	switch (header->cmd)
	{
	case CMD_LOGIN_RESULT:   //如果返回的是登录的结果
	{
		LoginResult* loginResult = (LoginResult*)header;
		//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std::endl;
	}
	break;
	case CMD_LOGOUT_RESULT:  //如果是退出的结果
	{
		LogoutResult* logoutResult = (LogoutResult*)header;
		//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std::endl;
	}
	break;
	case CMD_NEW_USER_JOIN:  //有新用户加入
	{
		NewUserJoin* newUserJoin = (NewUserJoin*)header;
		//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std::endl;
	}
	break;
	case CMD_ERROR:  //错误消息
	{
		//错误消息的类型就是DataHeader的,因此直接使用header即可
		//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_ERROR,数据长度:" << header->dataLength << std::endl;
	}
	break;
	default:
	{
		//std::cout << "<Socket=" << _sock << ">,收到服务端数据:未知类型的消息,数据长度:" << header->dataLength << std::endl;
	}
	}
}

int EasyTcpClient::SendData(DataHeader* header,int nLen) 
{
	int ret = SOCKET_ERROR;
	if (isRun() && header)
	{
		ret = send(_sock, (const char*)header, nLen, 0);
		if (ret == SOCKET_ERROR) {
			CloseSocket();
			printf("Client:socket<%d>发送数据错误,关闭客户端连接\n", static_cast<int>(_sock));
		}
	}
	return ret;
}

#endif // !_EasyTcpClient_hpp_

client.cpp

  • 代码如下,因为客户端只发送数据不接收数据,因此需要将第97行的代码注释掉,让其不接收数据
#include "EasyTcpClient.hpp"
#include <thread>

bool g_bRun = false;
const int cCount = 10000; //客户端的数量
const int tCount = 4;    //线程的数量
EasyTcpClient* client[cCount];//客户端的数组


void cmdThread();
void sendThread(int id);

int main()
{
	g_bRun = true;

	//UI线程,可以输入命令
	std::thread t(cmdThread);
	t.detach();

	//启动发送线程
	for (int n = 0; n < tCount; ++n)
	{
		std::thread t(sendThread, n + 1);
		t.detach();
	}

	while (g_bRun)
		Sleep(100);

	/*getchar();
	getchar();*/
	return 0;
}

void cmdThread()
{
	char cmdBuf[256] = {};
	while (true)
	{
		std::cin >> cmdBuf;
		if (0 == strcmp(cmdBuf, "exit"))
		{
			g_bRun = false;
			break;
		}
		else {
			std::cout << "命令不识别,请重新输入" << std::endl;
		}
	}
}

void sendThread(int id)
{
	/*
		下面这几个变量是为了平均每个线程创建的客户端的数量:
			例如,本次测试时客户端数量为1000,线程数量为4,那么每个线程应该创建250个客户端
				线程1:c=250,begin=0,end=250
				线程2:c=250,begin=250,end=500
				线程3:c=250,begin=500,end=750
				线程4:c=250,begin=750,end=1000
	*/
	int c = cCount / tCount;
	int begin = (id - 1)*c;
	int end = id*c;

	for (int n = begin; n < end; ++n) //创建客户端
	{
		client[n] = new EasyTcpClient;
	}
	for (int n = begin; n < end; ++n) //让每个客户端连接服务器
	{
		client[n]->ConnectServer("192.168.0.105", 4567);
	}
	printf("Thread<%d>,Connect=<begin=%d, end=%d>\n", id, (begin + 1), end);

	//连接成功之后,等待3秒再去发送数据
	std::chrono::microseconds t(3000);
	std::this_thread::sleep_for(t);

	//这里定义为数组,可以随根据需求修改客户端单次发送给服务端的数据包数量
	const int nNum = 1;
	Login login[nNum];
	for (int n = 0; n < nNum; ++n)
	{
		strcpy(login[n].userName, "dongshao");
		strcpy(login[n].PassWord, "123456");
	}
	//在外面定义nLen,就不用每次在for循环中SendData时还要去sizeof计算一下login的大小
	int nLen = sizeof(login);
	//循环向服务端发送消息
	while (g_bRun)
	{
		for (int n = begin; n < end; ++n)
		{
			client[n]->SendData(login, nLen);
			//client[n]->Onrun();
		}
	}

	//关闭客户端
	for (int n = begin; n < end; ++n)
	{
		client[n]->CloseSocket();
		delete client[n];
	}

	printf("thread:all clients close the connection!\n");
}

三、服务端代码如下

EasyTcpServer.hpp

#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_

#ifdef _WIN32
	#define FD_SETSIZE 10240
	#define WIN32_LEAN_AND_MEAN
	#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
	#define _CRT_SECURE_NO_WARNINGS
	#include <windows.h>
	#include <WinSock2.h>
	#pragma comment(lib, "ws2_32.lib")
#else
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>
	//在Unix下没有这些宏,为了兼容,自己定义
	#define SOCKET int
	#define INVALID_SOCKET  (SOCKET)(~0)
	#define SOCKET_ERROR            (-1)
#endif


#ifndef RECV_BUFF_SIZE
#define RECV_BUFF_SIZE 10240        //接收缓冲区的大小
#endif // !RECV_BUFF_SIZE

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <vector>
#include <mutex>
#include <atomic>
#include <functional> 
#include "MessageHeader.hpp"
#include "CELLTimestamp.hpp"
//using namespace std;

//客户端数据类型,用来封装一个客户端
class ClientSocket
{
public:
	ClientSocket(SOCKET sockfd= INVALID_SOCKET) :_sock(sockfd), _lastPos(0) {
		memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));
	}

	SOCKET sockfd() { return _sock; }
	char   *msgBuff() { return _recvMsgBuff; }
	int    getLastPos() { return _lastPos; }
	void   setLastPos(int pos) { _lastPos = pos; }

	int    SendData(DataHeader* header) {
		if (header) {
			return send(_sock, (const char*)header, header->dataLength, 0);
		}
		return SOCKET_ERROR;
	}
private:
	SOCKET _sock;                             //客户端socket
	char   _recvMsgBuff[RECV_BUFF_SIZE * 5];  //消息接收缓冲区
	int    _lastPos;                          //缓冲区的数据尾部位置
};

//网络事件接口
class INetEvent {
public:
	virtual void OnClientJoin(ClientSocket* pClient) = 0;     //客户端加入事件
	virtual void OnClientLeave(ClientSocket* pClient) = 0;    //客户端离开事件
	virtual void OnNetMsg(ClientSocket* pClient, DataHeader* header) = 0; //接收到客户端消息事件
};

class CellServer
{
public:
	CellServer(SOCKET sock = INVALID_SOCKET) :_sock(sock), maxSock(_sock), _pthread(nullptr), _pNetEvent(nullptr){
		memset(_recvBuff, 0, sizeof(_recvBuff));
	}
	~CellServer() { CloseSocket(); }
public:
	bool   isRun() { return _sock != INVALID_SOCKET; }
	void   CloseSocket();
public:
	size_t getClientCount() { return _clients.size() + _clientsBuff.size(); } //返回当前客户端的数量
	void   setEventObj(INetEvent* event) { _pNetEvent = event; }              //设置事件对象,此处绑定的是EasyTcpServer
public:
	bool   Onrun();
	void   AddClient(ClientSocket* pClient)  //讲客户端加入到客户端连接缓冲队列中
	{
		//自解锁
		std::lock_guard<std::mutex> lock(_mutex); 
		//_mutex.lock(); 当然也可以使用互斥锁
		_clientsBuff.push_back(pClient);
		//_mutex.unlock();
	}
	int    RecvData(ClientSocket* pClient);     //接收数据
	void   OnNetMessage(ClientSocket* pClient, DataHeader* header);//处理网络消息
	void   Start() {
		//启动当前服务线程
		//创建一个线程,线程执行函数为Onrun(),其实可以不传递this,但是为了更安全,可以传递this给Onrun()
		_pthread = new std::thread(std::mem_fn(&CellServer::Onrun), this);
	}
private:
	SOCKET                     _sock;        //服务端的套接字
	std::vector<ClientSocket*> _clients;     //真正存储客户端
	std::vector<ClientSocket*> _clientsBuff; //存储客户端连接缓冲队列,之后会被加入到_clients中去
	SOCKET                     maxSock;      //当前最大的文件描述符值,select的参数1要使用
	char _recvBuff[RECV_BUFF_SIZE];          //接收缓冲区
	std::mutex                 _mutex;       //互斥锁
	std::thread*               _pthread;     //当前子服务端执行的线程
	INetEvent*                 _pNetEvent;
};

class EasyTcpServer:public INetEvent
{
public:
	EasyTcpServer() :_sock(INVALID_SOCKET), _recvCount(0), _clientCount(0){}
	virtual ~EasyTcpServer() { CloseSocket(); }
public:
	void InitSocket();  //初始化socket
	int  Bind(const char* ip, unsigned short port);    //绑定端口号
	int  Listen(int n); //监听端口号
	SOCKET Accept();    //接收客户端连接
	void addClientToCellServer(ClientSocket* pClient);//将新客户加入到CellServer的客户端连接缓冲队列中
	void Start(int nCellServer);                      //创建从服务器,并运行所有的从服务器。(参数为从服务器的数量)
	void CloseSocket(); //关闭socket
	bool isRun() { return _sock != INVALID_SOCKET; }  //判断当前服务端是否在运行
	bool Onrun();       //处理网络消息

	void time4msg();    //每1秒统计一次收到的数据包的数量
public:
	//客户端加入事件
	//这个是线程安全的,因为其只会被主服务器(自己)调用
	virtual void OnClientJoin(ClientSocket* pClient)override { _clientCount++; }

	//客户端离开事件,这个里面做的事情比较简单,只是将当前客户端的数量--(如果从服务器不止一个,那么此函数不是线程安全的,因为这个函数会被多个从服务器调用的)
	virtual void OnClientLeave(ClientSocket* pClient)override { _clientCount--; }

	//接收到客户端消息事件,将数据包的数量增加(如果从服务器不止一个,那么此函数不是线程安全的,因为这个函数会被多个从服务器调用的)
	virtual void OnNetMsg(ClientSocket* pClient, DataHeader* header)override { _recvCount++; }
private:
	SOCKET                     _sock;        //服务端套接字
	std::vector<CellServer*>   _cellServers; //存放从服务端对象
	CELLTimestamp              _tTime;       //计时器
	std::atomic_int            _recvCount;   //表示服务端接收到客户端数据包的数量(这里采用一个原子操作,没什么特殊原因,使用玩玩罢了)
	std::atomic_int            _clientCount; //客户端的数量
};

void EasyTcpServer::InitSocket()
{
#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif

	//建立socket
	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "Server:创建socket成功" << std::endl;
	}
	else {
		std::cout << "Server:创建socket成功" << std::endl;
	}
}

int EasyTcpServer::Bind(const char* ip, unsigned short port)
{
	if (!isRun())
		InitSocket();

	//初始化服务端地址
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	if (ip) 
		_sin.sin_addr.S_un.S_addr = inet_addr(ip);
	else
		_sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
	if (ip)
		_sin.sin_addr.s_addr = inet_addr(ip);
	else
		_sin.sin_addr.s_addr = INADDR_ANY;
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(port);

	//绑定服务端地址
	int ret = ::bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		if (ip)
			std::cout << "Server:绑定地址(" << ip << "," << port << ")失败!" << std::endl;
		else
			std::cout << "Server:绑定地址(INADDR_ANY," << port << ")失败!" << std::endl;
	}
	else {
		if (ip)
			std::cout << "Server:绑定地址(" << ip << "," << port << ")成功!" << std::endl;
		else
			std::cout << "Server:绑定地址(INADDR_ANY," << port << ")成功!" << std::endl;
	}
	return ret;
}

void EasyTcpServer::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		closesocket(_sock);
		WSACleanup();
#else
		close(_sock);
#endif
		_sock = INVALID_SOCKET;
	}
}

int EasyTcpServer::Listen(int n)
{
	//监听网络端口
	int ret = listen(_sock, n);
	if (SOCKET_ERROR == ret)
		std::cout << "Server:监听网络端口失败!" << std::endl;
	else
		std::cout << "Server:监听网络端口成功!" << std::endl;
	return ret;
}

SOCKET EasyTcpServer::Accept()
{
	//用来保存客户端地址
	struct sockaddr_in _clientAddr = {};
	int nAddrLen = sizeof(_clientAddr);
	SOCKET _cSock = INVALID_SOCKET;

	//接收客户端连接
#ifdef _WIN32
	_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
#else
	_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, (socklen_t*)&nAddrLen);
#endif
	if (INVALID_SOCKET == _cSock) {
		std::cout << "Server:接收到无效客户端!" << std::endl;
	}
	else {
		//通知其他已存在的所有客户端,有新的客户端加入
		//NewUserJoin newUserInfo(static_cast<int>(_cSock));
		//SendDataToAll(&newUserInfo);

		//将新连接的客户端加入到从服务器的客户端缓冲队列中
		ClientSocket* newClient = new ClientSocket(_cSock);
		addClientToCellServer(newClient);
		OnClientJoin(newClient); //相应客户端加入事件,其函数会将客户端的数量++

		//std::cout << "Server:接受到新的客户端(" << _clients.size() << ")连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
		//	<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
	}
	return _cSock;
}

void EasyTcpServer::addClientToCellServer(ClientSocket* pClient)
{
	//在_cellServers中寻找,哪一个CellServer其处理的客户数量最少,那么就将新客户加入到这个CellServer对象中去
	auto pMinServer = _cellServers[0];
	for (auto pCellServer : _cellServers)
	{
		if (pMinServer->getClientCount() > pCellServer->getClientCount())
		{
			pMinServer = pCellServer;
		}
	}
	pMinServer->AddClient(pClient);
}

void EasyTcpServer::Start(int nCellServer)
{
	for (int i = 0; i < nCellServer; ++i)
	{
		auto ser = new CellServer(_sock);
		_cellServers.push_back(ser);
		ser->setEventObj(this);
		ser->Start();
	}
}

bool EasyTcpServer::Onrun()
{
	if (isRun())
	{
		time4msg(); //统计当前接收到的数据包的数量

		fd_set fdRead;
		//fd_set fdWrite;
		//fd_set fdExp;
		FD_ZERO(&fdRead);
		//FD_ZERO(&fdWrite);
		//FD_ZERO(&fdExp);
		FD_SET(_sock, &fdRead);
		//FD_SET(_sock, &fdWrite);
		//FD_SET(_sock, &fdExp);

		struct timeval t = { 0,0 };
		int ret = select(_sock + 1, &fdRead, nullptr, nullptr, &t);
		if (ret < 0)
		{
			std::cout << "Server:select出错!" << std::endl;
			//select出错,那么就不能再继续运行select,出错之后,调用CloseSocket(),
			//关闭所有服务端、及客所有户端套接字,那么isRun()就会返回false,从而终止server.cpp程序运行
			CloseSocket();
			return false;
		}
		if (FD_ISSET(_sock, &fdRead))//如果一个客户端连接进来,那么服务端的socket就会变为可读的,此时我们使用accept来接收这个客户端
		{
			FD_CLR(_sock, &fdRead);
			Accept();
			return true;
		}
		return true;
	}
	return false;
}

void EasyTcpServer::time4msg()
{
	auto t = _tTime.getElapsedSecond();
	if (t >= 1.0)
	{
		//recvCount为什么要除以t1:因为我们要每1秒钟打印一次接收到的数据包,如果这个函数调用的时候时间差大于1秒,那么可以将recvCount/t,获得比较平均的数据包数量
		printf("time<%lf>,thread numer<%d>,client number<%d>,recvCount<%d>\n",
			t, _cellServers.size(), static_cast<int>(_clientCount), static_cast<int>((_recvCount / t)));
		_recvCount = 0;
		_tTime.update();
	}
}

void CellServer::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		//将所有的客户端套接字关闭
		for (int n = (int)_clients.size() - 1; n >= 0; --n)
		{
			closesocket(_clients[n]->sockfd());
			delete _clients[n];
		}
		//关闭服务端套接字
		closesocket(_sock);

		//因为这是从服务器,所以就不要清理套接字环境了,放置主服务器的套接字环境被清除
		//WSACleanup();
#else
		for (int n = (int)_clients.size() - 1; n >= 0; --n)
		{
			close(_clients[n]->sockfd());
			delete _clients[n];
		}
		close(_sock);
#endif
		_clients.clear();
		_sock = INVALID_SOCKET;
		delete _pthread;
	}
}

bool CellServer::Onrun()
{
	while (isRun())
	{
		//如果客户端缓冲队列_clientsBuff中有新客户,那么就将其加入到_clients中
		if (_clientsBuff.size() > 0)
		{
			//自解锁lock_guard,作用域结束后自动释放锁,因此if执行结束之后,_mutex就被释放了
			std::lock_guard<std::mutex> lock(_mutex);
			for (auto pClient : _clientsBuff)
			{
				_clients.push_back(pClient);
			}
			_clientsBuff.clear();
		}

		//如果没有客户,那么休眠一秒然后继续下一次循环
		if (_clients.empty())
		{
			std::chrono::milliseconds t(1);
			std::this_thread::sleep_for(t);
			continue;
		}

		fd_set fdRead;
		FD_ZERO(&fdRead);
		//在主线程的select中已经对主服务端的_sock进行查询了,所以从服务器就不需要了。否则两个地方同时操作会出错
		//FD_SET(_sock, &fdRead);

		for (int n = (int)_clients.size() - 1; n >= 0; --n)
		{
			FD_SET(_clients[n]->sockfd(), &fdRead);
			if (maxSock < _clients[n]->sockfd())
				maxSock = _clients[n]->sockfd();
		}

		//从服务器一般只用来收取数据,所以这里设置为阻塞的也可以
		//struct timeval t = { 1,0 };
		int ret = select(maxSock + 1, &fdRead, nullptr, nullptr, nullptr);
		if (ret < 0)
		{
			std::cout << "Server:select出错!" << std::endl;
			CloseSocket();
			return false;
		}

		//遍历vector数组中所有的客户端套接字,如果某个客户端的套接字在读集中,
		//那么说明相应的客户端有数据来,那么就执行processor()函数
		for (int n = (int)_clients.size() - 1; n >= 0; --n)
		{
			if (FD_ISSET(_clients[n]->sockfd(), &fdRead))
			{
				if (-1 == RecvData(_clients[n]))
				{
					//如果RecvData出错,那么就将该客户端从_client中移除
					auto iter = _clients.begin() + n;
					if (iter != _clients.end())
					{
						if (_pNetEvent)
							_pNetEvent->OnClientLeave(_clients[n]); //通知主服务器有客户端退出
						delete _clients[n];
						_clients.erase(iter);
					}
				}
			}
		}
	}
	return false;
}

int CellServer::RecvData(ClientSocket* pClient)
{
	int _nLen = recv(pClient->sockfd(), _recvBuff, RECV_BUFF_SIZE, 0);
	if (_nLen < 0) {
		//std::cout << "recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		//std::cout << "客户端<Socket=" << pClient->sockfd() << ">:已退出!" << std::endl;
		return -1;
	}

	memcpy(pClient->msgBuff() + pClient->getLastPos(), _recvBuff, _nLen);
	pClient->setLastPos(pClient->getLastPos() + _nLen);
	while (pClient->getLastPos() >= sizeof(DataHeader))
	{
		DataHeader* header = (DataHeader*)pClient->msgBuff();
		if (pClient->getLastPos() >= header->dataLength)
		{
			//剩余未处理消息缓冲区的长度
			int nSize = pClient->getLastPos() - header->dataLength;
			//处理网络消息
			OnNetMessage(pClient, header);
			//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移
			memcpy(pClient->msgBuff(), pClient->msgBuff() + header->dataLength, nSize);
			pClient->setLastPos(nSize);
		}
		else {
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

void CellServer::OnNetMessage(ClientSocket* pClient, DataHeader* header)
{
	//调用主服务OnNetMsg事件
	_pNetEvent->OnNetMsg(pClient, header);

	switch (header->cmd)
	{
	case CMD_LOGIN: //如果是登录
	{
		//Login *login = (Login*)header;
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的消息CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;

		//此处可以判断用户账户和密码是否正确等等(省略)

		//返回登录的结果给客户端
		//LoginResult ret;
		//pClient->SendData(&ret);
	}
	break;
	case CMD_LOGOUT:  //如果是退出
	{
		//Logout *logout = (Logout*)header;
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的消息CMD_LOGOUT,用户名:" << logout->userName << std::endl;

		//返回退出的结果给客户端
		//LogoutResult ret;
		//pClient->SendData(&ret);
	}
	break;
	default:  //如果有错误
	{
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的未知消息消息" << std::endl;

		//返回错误给客户端,DataHeader默认为错误消息
		//DataHeader ret;
		//pClient->SendData(&ret);
	}
	break;
	}
}

#endif

server.cpp

  • 代码如下:
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"

int main()
{
	EasyTcpServer server;
	server.Bind("192.168.0.105", 4567);
	server.Listen(5);
	server.Start(4);

	while (server.isRun())
	{
		server.Onrun();
	}

	server.CloseSocket();
	std::cout << "服务端停止工作!" << std::endl;

	getchar();  //防止程序一闪而过
	return 0;
}

四、使用VS Studio进行性能测试

第一步

  • 将EasyTcpServer项目设置为启动项

第二步

  • 点击性能探测器,然后点击开始

第三步

  • 开始之后服务端程序如右图所示,左侧为后来打开的客户端程序,可以看到有10000个个客户端连接到服务端,并且每秒大概接收但是多万个数据包

第四步

  • 运行一会儿后,点击停止收集,然后再创建详细的报告

第五步

  • 可以看到ws2_32.dll函数运行的最多(这个是win32的库函数,只要程序运行肯定就要调用库函数,因此这个不是我们研究的重点)
  • 再下面就是CellServer::Onrun()函数运行的最多,因此这个函数才是我们要研究的重点,下面主要分析这个函数的性能

  • 点击上面箭头所示的函数,进入下面的页面,首先看到399行Onrun()函数中FD_SET的性能消耗占33.4%。原因如下:
    • 主要是因为每次在select之前都需要将每个客户端再次存放到fd_set中,因此如果客户端数量较多,那个这么地方就会造成性能降低

  • 再下面就是select函数占用23.4的性能消耗

  • 再下面就是FD_ISSET和自定义数据接收函数RecvData()消耗一些性能

五、代码优化1(优化FD_SET)

  • 在上面的分析中我们可以看到,FD_SET()的函数性能消耗占比33.4(如下图所示),因为每次select之前我都需要把所有的客户端套接字都再次添加到fd_set中,因此性能消耗比较大

解决方案

  • 主要的思路是:
    • 在CellServer中定义两个新成员,一个名为_fdRead_bak,一个名为_clients_change
class CellServer
{
    //...其余同上
private:
    //...其余同上
    fd_set       _fdRead_bak;    //用来保存当前的fd_set
    bool         _clients_change;//当前是否有新客户端加入进来
};
  • CellServer::Onrun()修改代码如下,思路为:
    • _clients_change初始化为false,代表没有客户端加入。如果有新客户端加入,那么将_clients_change设置为true
    • select之前判断_clients_change:
      • 如果_clients_change为true,代表有新客户端,那么将所有的新客户端FD_SET加入到一个fd_set局部变量中(用于本次select使用),再将这个fd_set局部变量拷贝一份存储到_fdRead_bak中
      • 如果_clients_change为false,代表没有新客户端加入,那么就不需要再调用FD_SET将所有的客户端加入到fd_set中,直接将_fdRead_bak拷贝给fd_set局部变量即可
bool CellServer::Onrun()
{
	while (isRun())
	{
		//有新客户端,将其设置为true
		if (_clientsBuff.size() > 0)
		{
			//...代码同之前
			_clients_change = true;
		}

		//...代码同之前

		fd_set fdRead;
		FD_ZERO(&fdRead);

		//根据_fdRead_change判断是否有新客户端加入,如果有那么就进行新的FD_SET
		if (_clients_change)
		{
			//...代码同之前
			memcpy(&_fdRead_bak, &fdRead, sizeof(_fdRead_bak));
		}
		//否则直接拷贝,不用再循环FD_SET了
		else
			memcpy(&fdRead, &_fdRead_bak, sizeof(_fdRead_bak));
		

		//...代码同之前
	}
	return false;
}

重新进行性能测试

  • 性能测试的步骤与上面“四”介绍的步骤相同,现在我们将代码修改之后重新进行一次测试,结果如下所示,可以看到FD_SET的性能消耗降低为0.1%

六、代码优化2(优化FD_ISSET)

  • 在上面的分析中我们可以看到,FD_ISSET()的函数性能消耗占比8.2(如下图所示)

解决方案

  • 主要的思路为:
    • 因为FD_ISSET就是去判断fd_set集合中的套接字,又因为Windows与UNIX下fd_sert的结构不同,因此我们有两种方法:
      • 对于Windows:
        • 获取fd_set的fd_count,遍历fd_set,之后再进行数据的接收
      • 对于UNIX:
        • 还是跟之前一样,需要遍历_clients,然后使用FD_ISSET判断
  • 因为在UNIX下,还是需要遍历_clients,因此我们将_clients成员变量从vector类型改成了map类型,这样可以加快遍历的速度
class CellServer
{
	//同之前
private:
        //同之前
	std::map<SOCKET, ClientSocket*> _clients; //真正存储客户端
};

bool CellServer::Onrun()
{
	while (isRun())
	{
		//同之前
		int ret = select(maxSock + 1, &fdRead, nullptr, nullptr, nullptr);
		//同之前

#ifdef _WIN32
		//如果是WIN下运行,fd_set拥有fd_count与fd_array成员
		//我们可以遍历fd_set,然后从中获取数据,不需要使用FD_ISSET了
		for (int n = 0; n < fdRead.fd_count; n++)
		{
			auto iter = _clients.find(fdRead.fd_array[n]);
			//如果RecvData出错,那么就将该客户端从_client中移除
			if (-1 == RecvData(iter->second))
			{
				if (_pNetEvent)
					_pNetEvent->OnClientLeave(iter->second); //通知主服务器有客户端退出
				delete iter->second;
				_clients.erase(iter->first);
				_clients_change = true; //这个要设置为true,因为有客户端退出了,需要重新进行FD_SET
			}
		}
#else
		//如果在UNIX下,fd_set无fd_count与fd_array成员,我们只能遍历_clients数组
		//遍历_clients map容器中所有的客户端,然后从中获取数据
		for (auto iter : _clients)
		{
			//因为_clients是一个map,因此每次iter返回一个pair,其first成员为key(SOCKET),value成员为value(ClientSocket)
			if (FD_ISSET(iter.second->sockfd(), &fdRead))
			{
				//如果RecvData出错,那么就将该客户端从_client中移除
				if (-1 == RecvData(iter.second))
				{
					if (_pNetEvent)
						_pNetEvent->OnClientLeave(iter.second); //通知主服务器有客户端退出
					delete iter.second;
					_clients.erase(iter.first);
					_clients_change = true; //原因同上
				}
			}
		}
#endif // _WIN32

	}

	//同之前
}
  • 因为将_clients修改为map类型了,所以代码中别的地方用到了_clients,还需要一些修改(见文章最下面的服务端最终代码)

重新进行性能测试

  • 性能测试的步骤与上面“四”介绍的步骤相同,现在我们将代码修改之后重新进行一次测试,结果如下所示,可以看到判断fd_set集合这一块的代码性能消耗降低了

  • 现在的性能消耗主要都集中在select这里了

七、服务端最终代码

  • 因为测试的时候是客户端只发送数据,服务端只接收数据,所以CellServer::OnNetMessage()函数中的给客户端回送数据部分的代码被注释了
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_

#ifdef _WIN32
	#define FD_SETSIZE 10240
	#define WIN32_LEAN_AND_MEAN
	#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()
	#define _CRT_SECURE_NO_WARNINGS
	#include <windows.h>
	#include <WinSock2.h>
	#pragma comment(lib, "ws2_32.lib")
#else
	#include <unistd.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>
	//在Unix下没有这些宏,为了兼容,自己定义
	#define SOCKET int
	#define INVALID_SOCKET  (SOCKET)(~0)
	#define SOCKET_ERROR            (-1)
#endif


#ifndef RECV_BUFF_SIZE
#define RECV_BUFF_SIZE 10240        //接收缓冲区的大小
#endif // !RECV_BUFF_SIZE

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <vector>
#include <mutex>
#include <atomic>
#include <functional> 
#include <map>
#include "MessageHeader.hpp"
#include "CELLTimestamp.hpp"
//using namespace std;

//客户端数据类型,用来封装一个客户端
class ClientSocket
{
public:
	ClientSocket(SOCKET sockfd= INVALID_SOCKET) :_sock(sockfd), _lastPos(0) {
		memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));
	}

	SOCKET sockfd() { return _sock; }
	char   *msgBuff() { return _recvMsgBuff; }
	int    getLastPos() { return _lastPos; }
	void   setLastPos(int pos) { _lastPos = pos; }

	int    SendData(DataHeader* header) {
		if (header) {
			return send(_sock, (const char*)header, header->dataLength, 0);
		}
		return SOCKET_ERROR;
	}
private:
	SOCKET _sock;                             //客户端socket
	char   _recvMsgBuff[RECV_BUFF_SIZE * 5];  //消息接收缓冲区
	int    _lastPos;                          //缓冲区的数据尾部位置
};

//网络事件接口
class INetEvent {
public:
	virtual void OnClientJoin(ClientSocket* pClient) = 0;     //客户端加入事件
	virtual void OnClientLeave(ClientSocket* pClient) = 0;    //客户端离开事件
	virtual void OnNetMsg(ClientSocket* pClient, DataHeader* header) = 0; //接收到客户端消息事件
};

class CellServer
{
public:
	CellServer(SOCKET sock = INVALID_SOCKET) :_sock(sock), maxSock(_sock), _pthread(nullptr), _pNetEvent(nullptr), _clients_change(false){
		memset(_recvBuff, 0, sizeof(_recvBuff));
		memset(&_fdRead_bak, 0, sizeof(_fdRead_bak));
	}
	~CellServer() { CloseSocket(); }
public:
	bool   isRun() { return _sock != INVALID_SOCKET; }
	void   CloseSocket();
public:
	size_t getClientCount() { return _clients.size() + _clientsBuff.size(); } //返回当前客户端的数量
	void   setEventObj(INetEvent* event) { _pNetEvent = event; }              //设置事件对象,此处绑定的是EasyTcpServer
public:
	bool   Onrun();
	void   AddClient(ClientSocket* pClient)  //讲客户端加入到客户端连接缓冲队列中
	{
		//自解锁
		std::lock_guard<std::mutex> lock(_mutex); 
		//_mutex.lock(); 当然也可以使用互斥锁
		_clientsBuff.push_back(pClient);
		//_mutex.unlock();
	}
	int    RecvData(ClientSocket* pClient);     //接收数据
	void   OnNetMessage(ClientSocket* pClient, DataHeader* header);//处理网络消息
	void   Start() {
		//启动当前服务线程
		//创建一个线程,线程执行函数为Onrun(),其实可以不传递this,但是为了更安全,可以传递this给Onrun()
		_pthread = new std::thread(std::mem_fn(&CellServer::Onrun), this);
	}
private:
	SOCKET                     _sock;         //服务端的套接字
	std::map<SOCKET, ClientSocket*> _clients; //真正存储客户端
	std::vector<ClientSocket*> _clientsBuff;  //存储客户端连接缓冲队列,之后会被加入到_clients中去
	SOCKET                     maxSock;       //当前最大的文件描述符值,select的参数1要使用
	char _recvBuff[RECV_BUFF_SIZE];           //接收缓冲区
	std::mutex                 _mutex;        //互斥锁
	std::thread*               _pthread;      //当前子服务端执行的线程
	INetEvent*                 _pNetEvent;
	fd_set                     _fdRead_bak;   //用来保存当前的fd_set
	bool                       _clients_change;//当前是否有新客户端加入进来
};

class EasyTcpServer:public INetEvent
{
public:
	EasyTcpServer() :_sock(INVALID_SOCKET), _recvCount(0), _clientCount(0){}
	virtual ~EasyTcpServer() { CloseSocket(); }
public:
	void InitSocket();  //初始化socket
	int  Bind(const char* ip, unsigned short port);    //绑定端口号
	int  Listen(int n); //监听端口号
	SOCKET Accept();    //接收客户端连接
	void addClientToCellServer(ClientSocket* pClient);//将新客户加入到CellServer的客户端连接缓冲队列中
	void Start(int nCellServer);                      //创建从服务器,并运行所有的从服务器。(参数为从服务器的数量)
	void CloseSocket(); //关闭socket
	bool isRun() { return _sock != INVALID_SOCKET; }  //判断当前服务端是否在运行
	bool Onrun();       //处理网络消息

	void time4msg();    //每1秒统计一次收到的数据包的数量
public:
	//客户端加入事件
	//这个是线程安全的,因为其只会被主服务器(自己)调用
	virtual void OnClientJoin(ClientSocket* pClient)override { _clientCount++; }

	//客户端离开事件,这个里面做的事情比较简单,只是将当前客户端的数量--(如果从服务器不止一个,那么此函数不是线程安全的,因为这个函数会被多个从服务器调用的)
	virtual void OnClientLeave(ClientSocket* pClient)override { _clientCount--; }

	//接收到客户端消息事件,将数据包的数量增加(如果从服务器不止一个,那么此函数不是线程安全的,因为这个函数会被多个从服务器调用的)
	virtual void OnNetMsg(ClientSocket* pClient, DataHeader* header)override { _recvCount++; }
private:
	SOCKET                     _sock;        //服务端套接字
	std::vector<CellServer*>   _cellServers; //存放从服务端对象
	CELLTimestamp              _tTime;       //计时器
	std::atomic_int            _recvCount;   //表示服务端接收到客户端数据包的数量(这里采用一个原子操作,没什么特殊原因,使用玩玩罢了)
	std::atomic_int            _clientCount; //客户端的数量
};

void EasyTcpServer::InitSocket()
{
#ifdef _WIN32
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif

	//建立socket
	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "Server:创建socket成功" << std::endl;
	}
	else {
		std::cout << "Server:创建socket成功" << std::endl;
	}
}

int EasyTcpServer::Bind(const char* ip, unsigned short port)
{
	if (!isRun())
		InitSocket();

	//初始化服务端地址
	struct sockaddr_in _sin = {};
#ifdef _WIN32
	if (ip) 
		_sin.sin_addr.S_un.S_addr = inet_addr(ip);
	else
		_sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
	if (ip)
		_sin.sin_addr.s_addr = inet_addr(ip);
	else
		_sin.sin_addr.s_addr = INADDR_ANY;
#endif
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(port);

	//绑定服务端地址
	int ret = ::bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		if (ip)
			std::cout << "Server:绑定地址(" << ip << "," << port << ")失败!" << std::endl;
		else
			std::cout << "Server:绑定地址(INADDR_ANY," << port << ")失败!" << std::endl;
	}
	else {
		if (ip)
			std::cout << "Server:绑定地址(" << ip << "," << port << ")成功!" << std::endl;
		else
			std::cout << "Server:绑定地址(INADDR_ANY," << port << ")成功!" << std::endl;
	}
	return ret;
}

void EasyTcpServer::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		closesocket(_sock);
		WSACleanup();
#else
		close(_sock);
#endif
		_sock = INVALID_SOCKET;
	}
}

int EasyTcpServer::Listen(int n)
{
	//监听网络端口
	int ret = listen(_sock, n);
	if (SOCKET_ERROR == ret)
		std::cout << "Server:监听网络端口失败!" << std::endl;
	else
		std::cout << "Server:监听网络端口成功!" << std::endl;
	return ret;
}

SOCKET EasyTcpServer::Accept()
{
	//用来保存客户端地址
	struct sockaddr_in _clientAddr = {};
	int nAddrLen = sizeof(_clientAddr);
	SOCKET _cSock = INVALID_SOCKET;

	//接收客户端连接
#ifdef _WIN32
	_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
#else
	_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, (socklen_t*)&nAddrLen);
#endif
	if (INVALID_SOCKET == _cSock) {
		std::cout << "Server:接收到无效客户端!" << std::endl;
	}
	else {
		//通知其他已存在的所有客户端,有新的客户端加入
		//NewUserJoin newUserInfo(static_cast<int>(_cSock));
		//SendDataToAll(&newUserInfo);

		//将新连接的客户端加入到从服务器的客户端缓冲队列中
		ClientSocket* newClient = new ClientSocket(_cSock);
		addClientToCellServer(newClient);
		OnClientJoin(newClient); //相应客户端加入事件,其函数会将客户端的数量++

		//std::cout << "Server:接受到新的客户端(" << _clients.size() << ")连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
		//	<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
	}
	return _cSock;
}

void EasyTcpServer::addClientToCellServer(ClientSocket* pClient)
{
	//在_cellServers中寻找,哪一个CellServer其处理的客户数量最少,那么就将新客户加入到这个CellServer对象中去
	auto pMinServer = _cellServers[0];
	for (auto pCellServer : _cellServers)
	{
		if (pMinServer->getClientCount() > pCellServer->getClientCount())
		{
			pMinServer = pCellServer;
		}
	}
	pMinServer->AddClient(pClient);
}

void EasyTcpServer::Start(int nCellServer)
{
	for (int i = 0; i < nCellServer; ++i)
	{
		auto ser = new CellServer(_sock);
		_cellServers.push_back(ser);
		ser->setEventObj(this);
		ser->Start();
	}
}

bool EasyTcpServer::Onrun()
{
	if (isRun())
	{
		time4msg(); //统计当前接收到的数据包的数量

		fd_set fdRead;
		//fd_set fdWrite;
		//fd_set fdExp;
		FD_ZERO(&fdRead);
		//FD_ZERO(&fdWrite);
		//FD_ZERO(&fdExp);
		FD_SET(_sock, &fdRead);
		//FD_SET(_sock, &fdWrite);
		//FD_SET(_sock, &fdExp);

		struct timeval t = { 0,0 };
		int ret = select(_sock + 1, &fdRead, nullptr, nullptr, &t);
		if (ret < 0)
		{
			std::cout << "Server:select出错!" << std::endl;
			//select出错,那么就不能再继续运行select,出错之后,调用CloseSocket(),
			//关闭所有服务端、及客所有户端套接字,那么isRun()就会返回false,从而终止server.cpp程序运行
			CloseSocket();
			return false;
		}
		if (FD_ISSET(_sock, &fdRead))//如果一个客户端连接进来,那么服务端的socket就会变为可读的,此时我们使用accept来接收这个客户端
		{
			FD_CLR(_sock, &fdRead);
			Accept();
			return true;
		}
		return true;
	}
	return false;
}

void EasyTcpServer::time4msg()
{
	auto t = _tTime.getElapsedSecond();
	if (t >= 1.0)
	{
		//recvCount为什么要除以t1:因为我们要每1秒钟打印一次接收到的数据包,如果这个函数调用的时候时间差大于1秒,那么可以将recvCount/t,获得比较平均的数据包数量
		printf("time<%lf>,thread numer<%d>,client number<%d>,recvCount<%d>\n",
			t, _cellServers.size(), static_cast<int>(_clientCount), static_cast<int>((_recvCount / t)));
		_recvCount = 0;
		_tTime.update();
	}
}

void CellServer::CloseSocket()
{
	if (_sock != INVALID_SOCKET)
	{
#ifdef _WIN32
		//将所有的客户端套接字关闭
		for(auto iter:_clients)
		{
			closesocket(iter.second->sockfd());
			delete iter.second;
		}
		//关闭服务端套接字
		closesocket(_sock);

		//因为这是从服务器,所以就不要清理套接字环境了,放置主服务器的套接字环境被清除
		//WSACleanup();
#else
		for (auto iter : _clients)
		{
			close(iter.second->sockfd());
			delete iter.second;
		}
		close(_sock);
#endif
		_clients.clear();
		_sock = INVALID_SOCKET;
		delete _pthread;
	}
}

bool CellServer::Onrun()
{
	while (isRun())
	{
		//如果客户端缓冲队列_clientsBuff中有新客户,那么就将其加入到_clients中
		if (_clientsBuff.size() > 0)
		{
			//自解锁lock_guard,作用域结束后自动释放锁,因此if执行结束之后,_mutex就被释放了
			std::lock_guard<std::mutex> lock(_mutex);
			for (auto pClient : _clientsBuff)
			{
				_clients[pClient->sockfd()] = pClient;
			}
			_clientsBuff.clear();
			_clients_change = true;
		}

		//如果没有客户,那么休眠一秒然后继续下一次循环
		if (_clients.empty())
		{
			std::chrono::milliseconds t(1);
			std::this_thread::sleep_for(t);
			continue;
		}

		fd_set fdRead;
		FD_ZERO(&fdRead);
		//在主线程的select中已经对主服务端的_sock进行查询了,所以从服务器就不需要再将_sock加入到fd_set中了,否则两个地方同时操作会出错
		//FD_SET(_sock, &fdRead);

		//根据_fdRead_change判断是否有新客户端加入,如果有那么就进行新的FD_SET
		if (_clients_change)
		{
			_clients_change = false;
			for (auto iter : _clients)
			{
				FD_SET(iter.second->sockfd(), &fdRead);
				if (maxSock < iter.second->sockfd())
					maxSock = iter.second->sockfd();
			}
			//将更新后的fd_set保存到_fdRead_bak中
			memcpy(&_fdRead_bak, &fdRead, sizeof(_fdRead_bak));
		}
		//否则直接拷贝,不用再循环FD_SET了
		else
			memcpy(&fdRead, &_fdRead_bak, sizeof(_fdRead_bak));
		

		//从服务器一般只用来收取数据,所以这里设置为阻塞的也可以
		//struct timeval t = { 1,0 };
		int ret = select(maxSock + 1, &fdRead, nullptr, nullptr, nullptr);
		if (ret < 0)
		{
			std::cout << "Server:select出错!" << std::endl;
			CloseSocket();
			return false;
		}

#ifdef _WIN32
		//如果是WIN下运行,fd_set拥有fd_count与fd_array成员
		//我们可以遍历fd_set,然后从中获取数据,不需要使用FD_ISSET了
		for (int n = 0; n < fdRead.fd_count; n++)
		{
			auto iter = _clients.find(fdRead.fd_array[n]);
			//如果RecvData出错,那么就将该客户端从_client中移除
			if (-1 == RecvData(iter->second))
			{
				if (_pNetEvent)
					_pNetEvent->OnClientLeave(iter->second); //通知主服务器有客户端退出
				delete iter->second;
				_clients.erase(iter->first);
				_clients_change = true; //这个要设置为true,因为有客户端退出了,需要重新进行FD_SET
			}
		}
#else
		//如果在UNIX下,fd_set无fd_count与fd_array成员,我们只能遍历_clients数组
		//遍历_clients map容器中所有的客户端,然后从中获取数据
		for (auto iter : _clients)
		{
			//因为_clients是一个map,因此每次iter返回一个pair,其first成员为key(SOCKET),value成员为value(ClientSocket)
			if (FD_ISSET(iter.second->sockfd(), &fdRead))
			{
				//如果RecvData出错,那么就将该客户端从_client中移除
				if (-1 == RecvData(iter.second))
				{
					if (_pNetEvent)
						_pNetEvent->OnClientLeave(iter.second); //通知主服务器有客户端退出
					delete iter.second;
					_clients.erase(iter.first);
					_clients_change = true; //原因同上
				}
			}
		}
#endif // _WIN32

	}
	return false;
}

int CellServer::RecvData(ClientSocket* pClient)
{
	int _nLen = recv(pClient->sockfd(), _recvBuff, RECV_BUFF_SIZE, 0);
	if (_nLen < 0) {
		//std::cout << "recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		//std::cout << "客户端<Socket=" << pClient->sockfd() << ">:已退出!" << std::endl;
		return -1;
	}

	memcpy(pClient->msgBuff() + pClient->getLastPos(), _recvBuff, _nLen);
	pClient->setLastPos(pClient->getLastPos() + _nLen);
	while (pClient->getLastPos() >= sizeof(DataHeader))
	{
		DataHeader* header = (DataHeader*)pClient->msgBuff();
		if (pClient->getLastPos() >= header->dataLength)
		{
			//剩余未处理消息缓冲区的长度
			int nSize = pClient->getLastPos() - header->dataLength;
			//处理网络消息
			OnNetMessage(pClient, header);
			//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移
			memcpy(pClient->msgBuff(), pClient->msgBuff() + header->dataLength, nSize);
			pClient->setLastPos(nSize);
		}
		else {
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

void CellServer::OnNetMessage(ClientSocket* pClient, DataHeader* header)
{
	//调用主服务OnNetMsg事件
	_pNetEvent->OnNetMsg(pClient, header);

	switch (header->cmd)
	{
	case CMD_LOGIN: //如果是登录
	{
		//Login *login = (Login*)header;
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的消息CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;

		//此处可以判断用户账户和密码是否正确等等(省略)

		//返回登录的结果给客户端
		//LoginResult ret;
		//pClient->SendData(&ret);
	}
	break;
	case CMD_LOGOUT:  //如果是退出
	{
		//Logout *logout = (Logout*)header;
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的消息CMD_LOGOUT,用户名:" << logout->userName << std::endl;

		//返回退出的结果给客户端
		//LogoutResult ret;
		//pClient->SendData(&ret);
	}
	break;
	default:  //如果有错误
	{
		//std::cout << "服务端:收到客户端<Socket=" << pClient->sockfd() << ">的未知消息消息" << std::endl;

		//返回错误给客户端,DataHeader默认为错误消息
		//DataHeader ret;
		//pClient->SendData(&ret);
	}
	break;
	}
}

#endif

八、总结

  • 本文对服务端select模型做了性能分析,并对FD_ISSET和FD_SET进行了优化,但是没有对select本身进行优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值