Select模型处理多客户端(timeval非阻塞类型)

我们现在需要第五个参数去,让select不在一直阻塞服务器进度
所以先让我们看一下select模型
在这里插入图片描述
第五个参数的类型为timeval,所以我们先要定义一个timeval指针对象。

服务端添加了
1.使用vector容器,向所有客户端发送,其他客户端的登录信息。

// 加这个宏定义也可以规避sock2和window.h的重编译问题
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

//winsock2一定要在windows前,否则会有宏定义重编译问题。sock2是新库,windows是老库。
#include<WinSock2.h>
#include<Windows.h>
#include<iostream>
#include<vector>

//加入静态链接库,否则会出现问题:无法解析的外部符号 __imp__WSAStartup@8
//也可以在属性页、连接器、输入、附加依赖项中输入ws_32.lib
#pragma comment(lib,"ws2_32.lib")

//枚举定义命令,方便判断
enum CMD
{
    
    
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGOUT_RESULT,
	CMD_LOGOUT,
	CMD_NEW_USER_JOIN,
	CMD_ERROR
};


//消息结构体
struct DataHeader {
    
    
	short dataLength;//数据长度
	short cmd;//命令:接收的命令,服务器处理后反馈数据。
};


struct Login :public DataHeader {
    
    
	Login()
	{
    
    
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char PassWord[32];
};

struct LoginResult :public DataHeader {
    
    
	LoginResult()
	{
    
    
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
		result = 0;
	}
	int result;
};

struct LogoutResult :public DataHeader {
    
    
	LogoutResult()
	{
    
    
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
		result = 0;
	}
	int result;
};

struct NewUserJoin :public DataHeader {
    
    
	NewUserJoin()
	{
    
    
		dataLength = sizeof(NewUserJoin);
		cmd = CMD_NEW_USER_JOIN;
		sock = 0;
	}
	int sock;
};

struct Logout :public DataHeader {
    
    
	Logout()
	{
    
    
		dataLength = sizeof(Logout);
		cmd = CMD_LOGOUT;
	}
	char userName[32];
};

//创建一个全局动态数组,用来存入新加入的客户端
std::vector<SOCKET>g_clients;


//处理进程的函数
int processor(SOCKET _clientSock) {
    
    
	//第一次接受一个缓冲数据
	char szRecv[1024] = {
    
    };
	//5.接收客户端数据
	//第一次收了header的数据包,只剩下CMD的数据长度,指针移动到数据包包体位置
	int nLen = recv(_clientSock, szRecv, sizeof(DataHeader), 0);
	DataHeader* header = (DataHeader*)szRecv;

	if (nLen <= 0)
	{
    
    
		printf("客户端<%d>已经退出,任务结束。",_clientSock);
		return -1;
	}
	printf("收到命令:%d ,数据长度:%d\n", header->cmd, header->dataLength);
	//6.处理请求
	if (nLen >= sizeof(DataHeader))
		switch (header->cmd)
		{
    
    
		case CMD_LOGIN:
		{
    
    

			//取剩余的长度
			//  收数据包的sock       数据包读取起点               数据包读取长度
			recv(_clientSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Login* login = (Login*)szRecv;
			//判断用户名和密码是否正确
			std::cout << login->userName << "  " << login->PassWord << std::endl;
			char name[32] = "李逵";
			char pw[32] = "110";
			if (0 == (strcmp(name, login->userName) | strcmp(pw, login->PassWord)))
			{
    
    
				//这个数据包的长度为32+32+2+2=68
				printf("<Socket %d>输入正确!,name is %s , password is %s ,数据长度:%d \n",_clientSock, login->userName, login->PassWord, login->dataLength);
				//short 的长度为2
				std::cout << "sizeof cmd " << sizeof(login->cmd) << "  " << "sizeof datalength" << sizeof(login->dataLength) << std::endl;
			}

			else
				printf("<Socket %d>重新输入密码\n",_clientSock);
			LoginResult result;

			//7.发送请求
			//一定要先发送消息头,然后在发送消息体,这样才是一个完整的报文
			//传过来的header带有cmd,根据cmd选择参数命令。所以直接返回接收的header就行
			send(_clientSock, (char*)&result, sizeof(LoginResult), 0);
		}
		break;

		case CMD_LOGOUT:
		{
    
    

			recv(_clientSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Logout* logout = (Logout*)szRecv;
			printf("<Socket %d>登出!,name is %s ,数据长度:%d \n",_clientSock, logout->userName, logout->dataLength);

			LogoutResult result;
			send(_clientSock, (char*)&result, sizeof(LogoutResult), 0);
		}
		break;
		//定义默认的错误信息
		default:
		{
    
    
			DataHeader header = {
    
     0,CMD_ERROR };
			send(_clientSock, (char*)&header, sizeof(DataHeader), 0);
		}
		break;
		}
}

int main()
{
    
    

	//版本号
	WORD ver = MAKEWORD(2, 2);
	//数据指针
	WSADATA dat;
	//windows下socket的启动函数,启动socket环境
	WSAStartup(ver, &dat);

	//开始编写网络环境
	//1.建立一个socket。套接字inet6为最新,数据类型:数据流,网络类型tcp,udp
	//定义返回类型,用来储存socket数据(uint型)
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	//2 bind 绑定用于接受客户端连接的网络端口
	//sock地址的结构体
	sockaddr_in _sin = {
    
    };
	//网络类型
	_sin.sin_family = AF_INET;
	//端口号htons(host to net unsigned short)主机到网络的字节序的转换.用来寻找服务程序
	_sin.sin_port = htons(4567);
	//定义IP地址,主机不只一个IP地址。cmd中ipconfig可以查看。
	//如果纯内网127就可以。还可以屏蔽外网的访问。
	//S_un是一个联合体。
	//INADDR_ANY不限定网路地址。
	//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	_sin.sin_addr.S_un.S_addr = INADDR_ANY;
	//sock名,地址,地址长度。强制转换地址类型sockaddr结构体里的数据类型不利于我们直接填写,
	//addr_in的结构体与普通addr结构体相同,而且有常见的数据类型方便填写。
	if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))//判断是否绑定成功
	{
    
    
		//如果端口号被别的程序占用,就是绑定失败
		printf("ERROR,绑定用于接受客户端连接的网络端口失败\n");
	}
	else
	{
    
    
		printf("绑定网络端口成功\n");
	}

	//3.listen监听网络端口
	//最大等待5人来连接
	if (SOCKET_ERROR == listen(_sock, 5))
	{
    
    
		printf("ERROR,监听失败\n");
	}
	else
	{
    
    
		printf("监听成功\n");
	}

	

	//接收缓冲区
	char _recvBuf[128] = {
    
    };
	while (true)
	{
    
    
		//创建集合
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExp;
		//清空集合
		FD_ZERO(&fdRead);
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExp);

		//定义一个可操作socket集合的宏
		FD_SET(_sock, &fdRead);
		FD_SET(_sock, &fdWrite);
		FD_SET(_sock, &fdExp);

		//与int固定四个字节不同有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int.
		//使用Int既有可能浪费,又有可能范围不够大。
		
		//把新客户放到可读集合里查询一下
		//for (size_t n = 0; n < g_clients.size(); n++)
		for(int n=(int)g_clients.size()-1;n>=0;n--)
		{
    
    
			FD_SET(g_clients[n], &fdRead);
		}

		//检查时间为0秒,select则不会阻塞
		timeval t = {
    
     5,0 };

		//nfds是一个整数值,是指fd_set集合中所有的描述符的范围。而不是数量。即所有文件描述符最大值+1
		//windows中有count,自动处理,所以没有意义,填0即可,linux中没有count所以需要手动数据描述符范围+1
		int ret=select(_sock + 1, &fdRead, &fdWrite, &fdExp, &t);
		//返回值小于0,即证明出错误,直接跳出
		if (ret < 0)
		{
    
    
			printf("客户端已退出,任务结束.\n");
			break;
		}
		//判断socket是否在集合中,因为select每次查询完后,就会清空正在执行的socket
		if (FD_ISSET(_sock, &fdRead))
		{
    
    
			//先清理下标志位
			FD_CLR(_sock, &fdRead);

			//4 accept 等待客户端连接
			sockaddr_in clientAddr = {
    
    };
			//定义client返回的socket数据的长度
			int nAddrlen = sizeof(sockaddr_in);
			//定义一个无效的sock地址用来进行判断是否接收到客服端
			SOCKET _clientSock = INVALID_ATOM;

			//如果固定长读就在在循环外计算出发送数据的长度,节省计算时间
			//int bufSize = sizeof(msgBuf);
			//循环重复执行

			//新用户加入
			//返回一个客户端的socket网络地址
			_clientSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrlen);
			//判断
			if (INVALID_SOCKET == _clientSock)
			{
    
    
				printf("错误,接收到无效客户端SOCKET\n");
			}
			else {
    
    
				g_clients.push_back(_clientSock);
				//inet_ntoa 把客户端转换成可读的ip地址
				//inet_ntoa 过时了,需要定义一个宏define _WINSOCK_DEPRECATED_NO_WARNINGS

				// add the port name & IP name 
				printf("新客户端加入:IP=%s, socket= %d \n", inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
			}
			//当一个新客户端加入的时候,向每个客户端发送一个客户端加入的信息
			for (int n = (int)g_clients.size() - 1; n >= 0; n--)
			{
    
    
				//vector 从0开始计数
				NewUserJoin userJoin;
				send(g_clients[n], (const char*)&userJoin, sizeof(NewUserJoin), 0);
			}
			
		}
		//让processor()循环处理vector中每个客户端的数据包
		for(size_t n=0;n<fdRead.fd_count;n++)
		{
    
    
			//如果一个客户端返回-1,就证明这个客户端退出了,所以要在vector中抹去这个客户端
			if (-1 == processor(fdRead.fd_array[n]))
			{
    
    
				auto iter = std::find(g_clients.begin(), g_clients.end(), fdRead.fd_array[0]);
				//如果只有一个socket,那iter一定会等于最后一个,所以iter!=socket的end值
				if (iter != g_clients.end())
				{
    
    
					g_clients.erase(iter);
				}
			}
		}
		
		//测试是否阻塞
		std::cout << "Running other program" << std::endl;
		
	}
	//关闭全部socket
	for (size_t n = g_clients.size() - 1; n >= 0; n--)
	{
    
    
		closesocket(g_clients[n]);
	}

		//8. 关闭自身的套节字
		closesocket(_sock);
		//结束编写网路环境

		//结束socket
		WSACleanup();
		printf("任务结束。");
		getchar();
		return 0;
}

客户端添加了

1.select模型
2.自动发送数据包

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<WinSock2.h>
#include<Windows.h>
#include<iostream>

#pragma comment(lib,"ws2_32.lib")

//传输的数据结构,最简单的数据包
//每个函数的类型必须一样,而且在客户端和服务端传输和接收顺序一致,也就是内存对齐
//long类型的在64位编译器下就是64位,而32位编译器下就是32位,
//所以需要考虑平台和系统,关注是否内存对齐

enum CMD
{
    
    
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGOUT_RESULT,
	CMD_LOGOUT,
	CMD_NEW_USER_JOIN,
	CMD_ERROR
};


//消息结构体
struct DataHeader {
    
    
	short dataLength;//数据长度
	short cmd;//命令:接收的命令,服务器处理后反馈数据。
};
//定义登录的数据结构
//使用继承方式,是报文更完整,不容易出错,也不用每次单独定义dataheader
struct Login :public DataHeader {
    
    
	Login()
	{
    
    
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char PassWord[32];
};

struct LoginResult:public DataHeader {
    
    
	LoginResult()
	{
    
    
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};

struct LogoutResult:public DataHeader {
    
    
	LogoutResult()
	{
    
    
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int result;
};

struct NewUserJoin :public DataHeader {
    
    
	NewUserJoin()
	{
    
    
		dataLength = sizeof(NewUserJoin);
		cmd = CMD_NEW_USER_JOIN;
		sock = 0;
	}
	int sock;
};

struct Logout:public DataHeader {
    
    
	Logout()
	{
    
    
		dataLength = sizeof(Logout);
		cmd = CMD_LOGOUT;
	}
	char userName[32];
};

//处理进程的函数
int processor(SOCKET _serverSock) {
    
    
	//第一次接受一个缓冲数据
	char szRecv[1024] = {
    
    };
	//5.接收客户端数据
	//第一次收了header的数据包,只剩下CMD的数据长度,指针移动到数据包包体位置
	int nLen = recv(_serverSock, szRecv, sizeof(DataHeader), 0);
	DataHeader* header = (DataHeader*)szRecv;

	if (nLen <= 0)
	{
    
    
		printf("与服务器断开连接。\n", _serverSock);
		return -1;
	}
	printf("收到命令:%d ,数据长度:%d\n", header->cmd, header->dataLength);
	//6.处理请求
		switch (header->cmd)
		{
    
    
		case CMD_LOGIN_RESULT:
		{
    
    
			recv(_serverSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			LoginResult* loginret = (LoginResult*)szRecv;
			printf("收到服务端返回消息:CMD_LOGIN_RESULT,数据长度:%d\n", loginret->dataLength);

			
		}
		break;

		case CMD_LOGOUT:
		{
    
    
			recv(_serverSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			LogoutResult* logoutret = (LogoutResult*)szRecv;
			printf("收到服务端返回消息:CMD_LOGOUT_RESULT,数据长度:%d\n",  logoutret->dataLength);
			
		}
		break;
		case CMD_NEW_USER_JOIN:
		{
    
    
			recv(_serverSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			NewUserJoin* newuser = (NewUserJoin*)szRecv;
			printf("新的SOCKET:%d 加入进服务器\n", newuser->sock);
		
		}
		break;
		//定义默认的错误信息
		
		}
}

int main()
{
    
    
	//启动Windows socket 2.x环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
	//1.建立一个socket
	SOCKET _sock= socket(AF_INET,SOCK_STREAM,0);
	//多做判断才能创建一个健壮的代码
	if (INVALID_SOCKET == _sock)
	{
    
    
		printf("ERROR,建立socket失败\n");
	}
	else
	{
    
    
		printf("建立socket成功\n");
	}
	//2.链接服务器connet
	//这样写快速初始化结构体,要不这样就是乱的
	sockaddr_in _sin = {
    
    };
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	//error C4996: 'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings
	_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//sizeof()传入类型更安全,不应该是变量,防止传入的是一个指针
	int ret=connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == ret)
	{
    
    
		printf("ERROR,链接服务器失败\n");

	}
	else
	{
    
    
		printf("连接服务器成功\n");
	}

	
	while (true)
	{
    
    
		fd_set fdReads;
		FD_ZERO(&fdReads);
		FD_SET(_sock, &fdReads);

		timeval t = {
    
    5, 5};
		//客户端select网络模型
		int ret=select(_sock,&fdReads,NULL,NULL,&t);
		if (ret < 0)
		{
    
    
			printf("Select 模型结束");
			break;
		}

		if (FD_ISSET(_sock, &fdReads))
		{
    
    
			FD_CLR(_sock, &fdReads);
			if (-1 ==processor(_sock))
			{
    
    
				std::cout << "Select 任务结束\n" << std::endl;
				break;
			}
		}

		printf("空闲时间处理其他业务..\n");
		Login login;
		strcpy_s(login.userName, "李逵");
		strcpy_s(login.PassWord, "110");
		send(_sock, (const char*)&login, sizeof(Login), 0);
		//windows下的延迟
		Sleep(5000);
	}

	
	//4.关闭套件字closesocket
	closesocket(_sock);
	//清除Windows socket环境
	WSACleanup();
	printf("已退出\n");
	getchar();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/guanxunmeng8928/article/details/109578254
今日推荐