Select model handles multiple clients

Blocking mode (old)
Insert picture description hereSelect mode
Insert picture description here

1. Dealing with the blocked network model
2. Release the server
3. The client handles content while waiting for data feedback
4. Deals with multiple clients

select model

int(
	//_In_ 表示只传入,而不会被改变
	_In_ int nfds,//伯克利socket,windows下没意义,在linux有意义表最大描述符+1
	//_Inout_opt_ 输入且会输出的可操作对象
	_Inout_opt_ fd_set FAR * readfds,//fd_set(fds)是一个套接字集合,可读集合 
	_Inout_opt_ fd_set FAR * writefds,//可写集合
	_Inout_opt_ fd_set FAR* exceptfds,//有异常的集合
	_In_opt_ const struct timeval FAR *timeout//查询时间的延迟,在时间内没有查到任何集合,就会让select变成一个非阻塞模式
	);

fd_set model

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

typedef struct fd_set {
    
    
        u_int fd_count;               /* how many are SET? */
        //默认放64个socket
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

server code

#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_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 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("客户端已经退出,任务结束。");
		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("输入正确!,name is %s , password is %s ,数据长度:%d \n", login->userName, login->PassWord, login->dataLength);
				//short 的长度为2
				std::cout << "sizeof cmd " << sizeof(login->cmd) << "  " << "sizeof datalength" << sizeof(login->dataLength) << std::endl;
			}

			else
				printf("重新输入密码\n");
			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("登出!,name is %s ,数据长度:%d \n", 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);
		}

		//nfds是一个整数值,是指fd_set集合中所有的描述符的范围。而不是数量。即所有文件描述符最大值+1
		//windows中有count,自动处理,所以没有意义,填0即可,linux中没有count所以需要手动数据描述符范围+1
		int ret=select(_sock + 1, &fdRead, &fdWrite, &fdExp, NULL);
		//返回值小于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(size_t n=0;n<fdRead.fd_count;n++)
		{
			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);
				}
			}
		}
		
		
	}
	//关闭全部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;
}

Client does not need to modify

Runtime problems:
There is a problem with the dynamic libraryThe cause of the problem should be the stack out of bounds,
1. Debug, click on warning
Insert picture description here2. The error is in size_t
Insert picture description here
3. Click on size_t, press f12 to jump to size_t and
Insert picture description herefound in size_t, no matter in 64-bit or 32-bit, Both are unsigned types. Unsigned types cannot perform "- -" operations, because unsigned types can only represent positive numbers. So replace size_t with int.

//在vc中编译器可以自动把size_t自动转换成int,但是在其他编译器中会出错,所以还是手动强转。
for(int n=(int)g_clients.size()-1;n>=0;n--)
		{
    
    
			FD_SET(g_clients[n], &fdRead);
		}

2. View thread collection

1. Debug the server side and find that there is a 280 socket in the readable collection.
Insert picture description here
2. When you do not start the client, you find that the program server will block in the select statement and
Insert picture description here
start the client, the server will check that the readable socket will run Go on
Insert picture description here
and FD_CLR() only cleans up the counter but not the socket.
Insert picture description here
So if it is just a response type network service, the select() model is fine. Only if the server needs to actively push some parameters, then the last parameter in the select() function plays a role.

Guess you like

Origin blog.csdn.net/guanxunmeng8928/article/details/109531621
Recommended