C++网络编程学习:缓冲区溢出与粘包分包

网络编程学习记录

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows / Linux

笔记一:建立基础TCP服务端/客户端  点我跳转
笔记二:网络数据报文的收发  点我跳转
笔记三:升级为select网络模型  点我跳转
笔记四:跨平台支持Windows、Linux系统  点我跳转
笔记五:源码的封装  点我跳转
笔记六:缓冲区溢出与粘包分包  点我跳转
笔记七:服务端多线程分离业务处理高负载  点我跳转


笔记六

  • 网络编程学习记录
  • 一、关于缓冲区溢出
    • 1.缓冲区溢出的原因
    • 2.缓冲区溢出的处理方法
  • 二、粘包与分包
    • 1.粘包与分包的原因
    • 2.粘包与分包的处理方法
      • 2.1客户端升级思路
      • 2.2服务端升级思路
  • 三、升级后的源码及其详细注释
    • 1.客户端源码 TcpClient.hpp
    • 2.服务端源码 TcpServer.hpp

一、关于缓冲区溢出

1.缓冲区溢出的原因

  之前我们所编写的服务端与客户端的数据量都是很小的,且操作也不频繁,需要键入指令发送报文。

  我们可以尝试在之前客户端代码的循环里,不断发送一种数据包,且把数据包的大小加大到1000字节,会发现很快服务端和客户端就会出现问题——要么是数据接收出现问题,要么是服务端或者客户端程序直接卡掉。这里出现问题的原因就是socket内核缓冲区溢出。

  首先,send和recv函数并不是直接通过网卡操作。在使用send函数时,send函数首先把数据写入到发送缓冲区,随后通过网卡发出;在使用recv函数时,网卡首先把接收到的消息写入接收缓冲区,recv函数再从中copy数据。注意,上文中的两个缓冲区是存在于内核中的,并不是程序中自定义的缓冲区。

  我们在之前的源码中,recv的逻辑是先接收包头,随后根据包头接收包体。而当网卡接收数据太多时,我们接收一个包头的时间,网卡可能就新接收了两个完整的数据包,这就导致内核接收缓冲区里的数据量是在不断增加的,最终导致接收缓冲区溢出,造成无法正常发送以及程序阻塞的问题。

  举个例子,缓冲区就像一个浴缸,而我们是一个拿盆子舀水的人。我们之前先接收一个包头就相当于舀出一个包头那么多的水,随后再舀出包体那么多的水。舀了两次仅仅舀出一个报文那么多的水。如果浴缸放水的速度比较大的话,我们很容易就会处理不过来。最终造成浴缸溢出(缓冲区溢出)。

2.缓冲区溢出的处理方法

  接着看上文的例子,我们怎么能阻止浴缸(缓冲区)溢出呢?首先我们不大可能改变浴缸的大小,因为太过麻烦以及治标不治本,只要浴缸放水的时间够长,总会溢出。接着,舀水的速度我们也不好改变,因为一时半会是改不了的。那我们就只能改变舀水的次数和数量了。

  如何改变舀水的数量和次数?我们可以一次舀出足够多的水,随后再从舀出的水中分出想要数量的水,这样浴缸溢出的可能性就大大减少了。

  从代码层面来看上面的思路,只要我们程序内新建一个足够大的缓冲区,一次从内核缓冲区上recv足够的数据,就可以避免内核缓冲区溢出了。

  • 大概思路如下:
char _Recv_buf[4096];
int DataRecv
{
     
	//接收客户端发送的数据 
	int recv_len = recv(socket, _Recv_buf, 4096, 0);
	if(recv_len <= 0)
	{
     
		return -1;
	} 
	while(_Recv_buf内不为空)
	{
     
		处理_Recv_buf内的数据
	}
	return 0;
}

  但是这样会出现新的问题,即粘包与分包问题,请看下文。

二、粘包与分包

1.粘包与分包的原因

  上文中处理缓冲区溢出的思路是没有问题的,但是上文中的源码写法会存在问题。

  我们一次接收那么多数据,其中数据的界限是没有限定的,比如上文中是想要一次接收4096个字节。假如缓冲区内有5个1000字节大小的数据包,我们这次接收4096字节,等于说接收的数据中有4.096个数据包,其中就包含了新的问题。

  首先是粘包问题。即一次接收中含有多个数据包,这就导致数据包界限不清,粘在了一起。像上文中的4.096个包,接收端是不清楚的,接收端只知道有4096字节的数据,但是它不知道一个包是多大。所以我们可以通过包头来获取一个数据包的大小,由此来处理相应大小的数据以解决粘包问题。

  接着是分包问题。即一次接收中含有不完整的包。例如上文中的4096个字节,其中包含了4个完整的包,和一个包的前96个字节。对此,我们只能处理前4个完整的数据包。那么问题来了,对于上文中的缓冲区,由于recv函数每次都会覆盖这个缓冲区,这就导致缓冲区内无法存放未处理的消息。对于这个问题,我们可以新建一个缓冲区,来存放未处理的消息,实现双缓冲,即可处理分包问题。

  • TCP是面向数据流的协议,所以会出现粘包分包问题;UDP是面向消息的协议,每一个数据段都是一条消息,所以不会出现粘包分包问题。

2.粘包与分包的处理方法

2.1客户端升级思路

  首先是新建两个缓冲区,一个用来存放recv到的数据,一个用来存放所有待处理数据。首先第一个缓冲区recv到数据,随后把第一个缓冲区内的数据copy到第二个缓冲区内,即可实现数据的存放。随后处理数据之类的还是先获取包头,随后根据包头处理包体数据。

  • 大致思路如下:
	//接收数据
	char 接收缓冲区[4096]
	char 消息缓冲区[40960];
	int RecvData(SOCKET temp_socket)//处理数据 
	{
      
		//接收客户端发送的数据 
		int recv_len = recv(temp_socket, 接收缓冲区, 4096, 0);
		if(recv_len <= 0)
		{
     
			printf("与服务器断开连接,任务结束\n");
			return -1;
		} 
		1.将接收缓冲区的数据拷贝到消息缓冲区 
		while(2.判断消息缓冲区的数据长度是否大于等于包头长度) 
		{
     
			3.选出包头数据 //解决粘包问题
			if(4.判断消息缓冲区内数据长度是否大于等于报文长度) //解决少包问题 
			{
     
				5.响应数据 
				6.将处理过的消息移出消息缓冲区
			} 
		} 
		return 0;	
	}

2.2服务端升级思路

  与客户端整头思路相似,但是需要注意,服务端有多个连接,如果多个连接共用一个缓冲区会存在错误,所以每一个客户端连接都需要有自己的缓冲区。对此,我们可以新建一个客户端连接类,来存放每一个客户端的socket以及它的缓冲区。

  • 大致思路如下:
class 客户端连接
{
     
public:
	1.获取socket() 
	2.获取缓冲区()
	3.获取缓冲区长度()
	4.设置缓冲区长度()
private:	
	1.socket
	2.缓冲区
}; 

std::vector<客户端连接*> _clients;//储存客户端socket
char 接收缓冲区[4096];

0.此时前面OnRun函数里的判断过程也需要改变
//遍历所有socket 查看是否有待处理事件 
for(int n=0; n<_clients.size(); ++n)
{
     
	if(FD_ISSET(_clients[n]->获取socket(),&fdRead))
	{
     
		if(-1 == RecvData(_clients[n]))//处理请求 客户端退出的话 
		{
     
			std::vector<客户端连接*>::iterator iter = _clients.begin()+n;//找到退出客户端的地址
			if(iter != _clients.end())//如果是合理值
			{
     
				delete _clients[n];
				_clients.erase(iter);//移除
			}
		}
	}
}

int RecvData(客户端连接* client)//处理数据 
{
      
	//接收客户端发送的数据 
	int recv_len = recv(client->获取socket(), 接收缓冲区, 4096, 0);
	if(recv_len <= 0)
	{
     
		printf("与服务器断开连接,任务结束\n");
		return -1;
	} 
	1.将接收缓冲区的数据拷贝到传入对象的消息缓冲区 client->获取缓冲区();
	while(2.判断消息缓冲区的数据长度是否大于等于包头长度) client->获取缓冲区长度();
	{
     
		3.选出包头数据 //解决粘包问题
		if(4.判断消息缓冲区内数据长度是否大于等于报文长度) //解决少包问题 
		{
     
			5.响应数据 
			6.将处理过的消息移出消息缓冲区 client->设置缓冲区长度();
		} 
	} 
	return 0;	
}

三、升级后的源码及其详细注释

1.客户端源码 TcpClient.hpp

#ifndef _TcpClient_hpp_
#define _TcpClient_hpp_

#ifdef _WIN32
	#define WIN32_LEAN_AND_MEAN
	#include
	#include
	#pragma comment(lib,"ws2_32.lib") 
#else
	#include
	#include
	#include
	
	#define SOCKET int
	#define INVALID_SOCKET (SOCKET)(~0)
	#define SOCKET_ERROR (-1)
#endif
//枚举类型记录命令 
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];//密码 
	char data[932];
};
//包2 登录结果 传输结果
struct LoginResult : public DataHeader 
{
     
	LoginResult()//初始化包头 
	{
     
		this->cmd = CMD_LOGINRESULT;
		this->date_length = sizeof(LoginResult); 
	}
	int Result;
	char Data[992];//无意义数据 
};
//包3 登出 传输用户名 
struct Logout : public DataHeader 
{
     
	Logout()//初始化包头 
	{
     
		this->cmd = CMD_LOGOUT;
		this->date_length = sizeof(Logout); 
	}
	char UserName[32];//用户名 
	char data[964];
};
//包4 登出结果 传输结果
struct LogoutResult : public DataHeader 
{
     
	LogoutResult()//初始化包头 
	{
     
		this->cmd = CMD_LOGOUTRESULT;
		this->date_length = sizeof(LogoutResult); 
	}
	int Result;
	char data[992];
};
//包5 新用户登入 传输通告 
struct NewUserJoin : public DataHeader 
{
     
	NewUserJoin()//初始化包头 
	{
     
		this->cmd = CMD_NEW_USER_JOIN;
		this->date_length = sizeof(NewUserJoin); 
	}
	char UserName[32];//用户名 
};

#include
#define RECV_BUFFER_SIZE 4096

class TcpClient
{
     
public:
	//构造 
	TcpClient()
	{
     
		_sock = INVALID_SOCKET;
		//缓冲区相关 
		_Recv_buf = new char[RECV_BUFFER_SIZE];
		_Msg_buf = new char[RECV_BUFFER_SIZE*10];
		_Len_buf = 0; 
	}
	//析构 
	virtual ~TcpClient()
	{
     
		delete[] _Recv_buf;
		delete[] _Msg_buf;
		//关闭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("关闭连接\n",_sock); 
			CloseSocket();//如果之前有连接 就关闭连接 
		}
		_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); 
		if(INVALID_SOCKET == _sock)
		{
        

			return 0;//0为socket创建错误 
		} 
		return 1;
	} 
	
	//连接服务器  返回1为成功 
	int Connect(const char *ip,unsigned short port)
	{
     
		//如果为无效套接字 则初始化 
		if(INVALID_SOCKET == _sock)
		{
     
			InitSocket(); 
		}
		//连接服务器
	   	sockaddr_in _sin = {
     };
	   	_sin.sin_family = AF_INET;//IPV4
	   	_sin.sin_port = htons(port);//端口号 
#ifdef _WIN32
		_sin.sin_addr.S_un.S_addr =  inet_addr(ip);//IP 
#else
		_sin.sin_addr.s_addr =  inet_addr(ip);//IP 
#endif
		if(SOCKET_ERROR == connect(_sock,(sockaddr*)&_sin,sizeof(sockaddr_in)))
		{
     
			return 0;//连接失败 
		}
		else
		{
     
			return 1;//连接成功 
		}
	} 
	
	//关闭socket
	void CloseSocket()
	{
     
		if(INVALID_SOCKET != _sock) 
		{
     
#ifdef _WIN32
			//关闭socket
			closesocket(_sock); 
			//清除windows socket 环境 
			WSACleanup(); 
#else
			//关闭socket/LINUX
			close(_sock);
#endif
			_sock = INVALID_SOCKET;
		}
	} 
	
	//查询是否有待处理消息 
	bool OnRun()
	{
     
		if(IsRun())//如果有连接则监听事件 
		{
     
			fd_set _fdRead;//建立集合 
			FD_ZERO(&_fdRead);//清空集合  
			FD_SET(_sock,&_fdRead);//放入集合 
			timeval _t = {
     1,0};//select最大响应时间 
			//新建seclect 
			int _ret = select(_sock+1,&_fdRead,NULL,NULL,&_t);
			if(_ret<0)
			{
     
				printf("seclect任务结束\n");
				return false;
			}
			if(FD_ISSET(_sock,&_fdRead))//获取是否有可读socket 
			{
     
				FD_CLR(_sock,&_fdRead);//清理计数器 
				if(-1 == RecvData(_sock))
				{
     
					CloseSocket();
					return false;
				}
			}	
			return true;
		}
		return false;
	} 
	
	//判断是否工作中 
	bool IsRun()
	{
     
		return _sock != INVALID_SOCKET; 
	}
	
	//发送数据 
	int SendData(DataHeader *_head)
	{
     
		if(IsRun() && _head)
		{
     
			send(_sock,(const char*)_head,_head->date_length,0);
			return 1;
		}
		return 0;
	}
	
	//接收数据
	int RecvData(SOCKET temp_socket)//处理数据 
	{
      
		//接收客户端发送的数据 
		int recv_len = recv(temp_socket, _Recv_buf, RECV_BUFFER_SIZE, 0);
		if(recv_len <= 0)
		{
     
			printf("与服务器断开连接,任务结束\n");
			return -1;
		} 
		//将接收缓冲区的数据拷贝到消息缓冲区 
		memcpy(_Msg_buf+_Len_buf, _Recv_buf, recv_len); 
		//消息缓冲区的数据末尾后移 
		_Len_buf += recv_len; 
		//判断消息缓冲区的数据长度是否大于等于包头长度 
		while(_Len_buf >= sizeof(DataHeader))//处理粘包问题 
		{
     
			//选出包头数据 
			DataHeader* header = (DataHeader*)_Msg_buf; 
			//判断消息缓冲区内数据长度是否大于等于报文长度 避免少包问题 
			if(_Len_buf >= header->date_length)
			{
     
				//计算出消息缓冲区内剩余未处理数据的长度
				int size = _Len_buf - header->date_length; 
				//响应数据 
				NetMsg(header);
				//将消息缓冲区剩余未处理的数据前移
				memcpy(_Msg_buf, _Msg_buf + header->date_length, size);
				//消息缓冲区的数据末尾前移
				_Len_buf = size; 
			} 
			else
			{
     
				//消息缓冲区数据不足 
				break; 
			} 
		} 
		
		return 0;	
	}
	
	//响应数据
	virtual void NetMsg(DataHeader *_head)
	{
     
		printf("接收到包头,命令:%d,数据长度:%d\n",_head->cmd,_head->date_length);
		switch(_head->cmd)
		{
     
			case CMD_LOGINRESULT://登录结果 接收登录包体 
			{
     
				LoginResult *_result = (LoginResult*)_head; 
				printf("登录结果:%d\n",_result->Result);
			}
			break;
			case CMD_LOGOUTRESULT://登出结果 接收登出包体 
			{
     
				LogoutResult *_result = (LogoutResult*)_head; 
				printf("登录结果:%d\n",_result->Result);
			}
			break;
			case CMD_NEW_USER_JOIN://新用户登录通知 
			{
     
				NewUserJoin *_result = (NewUserJoin*)_head; 
				printf("用户:%s已登录\n",_result->UserName);
			} 
			break;
			case CMD_ERROR://错误 
			{
     
				printf("错误数据\n"); 
				getchar();
			} 
			break;
			default:
			{
     
				printf("未知数据\n"); 
				getchar();
			}
			break;
		}
	}
	
private:
	SOCKET _sock;
	//缓冲区相关 
	char *_Recv_buf;//接收缓冲区 
	char *_Msg_buf;//消息缓冲区 
	int _Len_buf;//缓冲区数据尾部变量 
};

#endif

2.服务端源码 TcpServer.hpp

#ifndef _TcpServer_hpp_
#define _TcpServer_hpp_

#ifdef _WIN32
	#define WIN32_LEAN_AND_MEAN
	#include
	#include
	#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有 
#else
	#include//selcet
	#include//uni std
	#include
	
	#define SOCKET int
	#define INVALID_SOCKET (SOCKET)(~0)
	#define SOCKET_ERROR (-1)
#endif
//枚举类型记录命令 
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];//密码 
	char data[932];
};
//包2 登录结果 传输结果
struct LoginResult : public DataHeader 
{
     
	LoginResult()//初始化包头 
	{
     
		this->cmd = CMD_LOGINRESULT;
		this->date_length = sizeof(LoginResult); 
	}
	int Result;
	char Data[992];//无意义数据 
};
//包3 登出 传输用户名 
struct Logout : public DataHeader 
{
     
	Logout()//初始化包头 
	{
     
		this->cmd = CMD_LOGOUT;
		this->date_length = sizeof(Logout); 
	}
	char UserName[32];//用户名 
	char data[964];
};
//包4 登出结果 传输结果
struct LogoutResult : public DataHeader 
{
     
	LogoutResult()//初始化包头 
	{
     
		this->cmd = CMD_LOGOUTRESULT;
		this->date_length = sizeof(LogoutResult); 
	}
	int Result;
	char data[992];
};
//包5 新用户登入 传输通告 
struct NewUserJoin : public DataHeader 
{
     
	NewUserJoin()//初始化包头 
	{
     
		this->cmd = CMD_NEW_USER_JOIN;
		this->date_length = sizeof(NewUserJoin); 
	}
	char UserName[32];//用户名 
};

#include
#define RECV_BUFFER_SIZE 4096

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 TcpServer
{
     
public:
	//构造 
	TcpServer()
	{
     
		_sock = INVALID_SOCKET; 
		//缓冲区相关 
		_Recv_buf = new char[RECV_BUFFER_SIZE];
	}
	
	//析构 
	virtual ~TcpServer()
	{
     
		delete[] _Recv_buf;
		//关闭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("关闭连接\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\n",temp_socket);
			return 0;
		}
		else
		{
      
			printf("新客户端加入\nIP地址为:%s \n", inet_ntoa(clientAddr.sin_addr));  
			//群发所有客户端 通知新用户登录 
			NewUserJoin *user_join = new NewUserJoin(); 
			strcpy(user_join->UserName,inet_ntoa(clientAddr.sin_addr));
			SendDataToAll(user_join);
			//将新的客户端加入动态数组
			_clients.push_back(new ClientSocket(temp_socket));
			return 1;
		} 
	} 
	
	//关闭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 OnRun()
	{
     
		if(IsRun())
		{
     
			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 = {
     2,0};//select最大响应时间 
			SOCKET maxSock = _sock;//最大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,&fdWrite,&fdExcept,&s_t); 
			if(ret<0)
			{
     
				printf("select任务结束\n");
				CloseSocket();
				return false;
			}
			if(FD_ISSET(_sock,&fdRead))//获取是否有新socket连接 
			{
     
				FD_CLR(_sock,&fdRead);//清理
				Accept();//连接 
			}
			//遍历所有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())//如果是合理值
						{
     
							delete _clients[n];
							_clients.erase(iter);//移除
						}
					}
				}
			}
			//printf("空闲时间处理其他业务\n");
			return true;
		}
		return false;
	} 
	
	//判断是否工作中 
	bool IsRun()
	{
     
		return _sock != INVALID_SOCKET; 
	}
	
	//发送数据 
	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());	
		} 
	} 
	
	//接收数据
	int RecvData(ClientSocket *t_client)//处理数据 
	{
      
		//接收客户端发送的数据  
		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;
		}
	}
	 
private:
	SOCKET _sock; 
	std::vector<ClientSocket*> _clients;//储存客户端
	//缓冲区相关 
	char *_Recv_buf;//接收缓冲区 
};

#endif

你可能感兴趣的:(网络编程,C/C++,网络,socket,网络通信,c++,tcpip)