C++ Socket网络编程 1.3版本 将服务端升级为Select模型处理多客户端

在之前的版本(1.1和1.2)中,服务端和客户端都是1对1的阻塞模型网络程序,例如服务端的accept,recv都是阻塞等待。一个最大的问题是,当服务端阻塞时,不能处理其它的业务,从而无法实现一个服务端处理多个客户端的功能。因此,将服务端升级为Select模型处理多客户端的网络请求。

Select网络模型是Linux、Windows、IOS、安卓等操作系统中标准C/C++支持的网络通信模型,内部会定时查询是否有新的客户端连接/新的业务需要处理,从而做出相应的处理。掌握了Select网络模型,便可以支持编写中、小型的网络程序。

在这里插入图片描述
Socket Select网络模型在Windows下面的定义

select(
    _In_ int nfds,		//在windows下面没有意义  在Unix衍生的操作系统中使用  
    _Inout_opt_ fd_set FAR * readfds, 		 //_Inout_  表示这个参数传入时是有意义的 传出时也是有意义的   指向检查可读性的套接字集合的可选的指针。表示客户端有读的socket客户端需求等待处理
    _Inout_opt_ fd_set FAR * writefds,		//指向检查可写性的套接字集合的可选的指针。表示客户端有写的socket客户端需求等待处理  
    _Inout_opt_ fd_set FAR * exceptfds,	//指向检查错误的套接字集合的可选的指针。表示客户端有异常的Socket客户端的需求等待处理
    _In_opt_ const struct timeval FAR * timeout 	//函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null。 
    );

select函数的理解:(转载自https://www.cnblogs.com/zxllm/p/5420065.html)
select函数用于决定一个或者多个socket的状态。对于每一个socket,客户端可以请求读、写或者错误状态信息。一个请求给定状态的socket集由fd_set结构体指定。在fd_set结构体中的socket必须和单个服务端联系在一起。select函数返回满足条件的套接字个数。fd_set集合可以通过一些宏手动操作。

(1)参数readfds指示检查socket的可读性。当socket在listen状态(服务端socket),如果已经接收一个连接请求,这个socket会被标记为可读,例如一个accept会确保不会阻塞的完成。对于其他的socket(客户端socket),可读性意味着队列中的数据适合读,当调用recv后不会阻塞。
(2)参数writefds指示检查socket的可写性。如果socket处理connect调用(非阻塞的),并且完全建立连接,这时socket是可写。如果socket没有处理connect调用,可写性意味着担保send执行成功。但是,如果len参数超过系统的缓存空间大小,它们在阻塞socket是可以阻塞的。不确定多长的长度是合法的,尤其在多线程环境下。
readfds:
① 如果listen函数已经调用并且连接挂起,accept会执行成功。
② 数据适合读(如果SO_OOBINLINE置位,包括OOB数据)
③ 连接被关/重置/终止
writefds:
① 如果处理一个connect调用(非阻塞),连接成功。
② 数据可以发送。
exceptfds:
① 如果处理一个connect调用(非阻塞),连接失败。
② OOB数据适合读(仅当SO_OOBINLINE未置位)

在头文件Winsock2.h中定义四个宏来操作和检查描述集。FD_SETSIZE决定在描述集合中最大数量(FD_SETSIZE的默认值为64,此值可以在导入Winsock2.h之前通过FD_SETSIZE修改)。
使用这些宏是为了在不同的套接字环境中维护软件便利。这些宏操作和检查fd_set内容为:
FD_CLR(s, *set) 从set集合中移除描述符s
FD_ISSET(s, *set) 如果s在set中,返回非0,否则返回0
FD_SET(s, *set) 增加描述符s到set中
FD_ZERO(*set) 初始化set集合为null集合

对服务端的数据进行改造,在前三步骤(建立socket,绑定端口和监听端口)之后。

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */     //SOCKET数组 
} fd_set;

服务端循环处理以下逻辑

	while (true)
	{
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExpect;

		FD_ZERO(&fdRead);		//清空fd集合的数据
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExpect);
		//这个宏的功能是 将服务端的_sock 放到fdRead这个集合中 
		//当socket在listen状态,如果已经接收一个连接请求,这个socket会被标记为可读,例如一个accept会确保不会阻塞的完成
		//对于其他的socket,可读性意味着队列中的数据适合读,当调用recv后不会阻塞。
		FD_SET(_sock, &fdRead);  //将服务端的socket放入可读列表,确保accept不阻塞
		FD_SET(_sock, &fdWrite);
		FD_SET(_sock, &fdExpect);

		for (size_t n = 0; n < g_clinets.size(); n++)
		{
			FD_SET(g_clinets[n], &fdRead);		//所有连入的客户端放入可读列表 保证recv不阻塞
		}

		//nfds第一个参数 是一个整数值 是指fd_set集合中所有socket值的范围 不是数量 
		int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExpect, NULL);
		if (ret < 0)
		{
			cout << "select任务结束" << endl;
			break;
		}
		if (FD_ISSET(_sock, &fdRead))	//判断_sock是否在fdRead中
		{
			FD_CLR(_sock, &fdRead); 
			//	4. 等待接受客户端连接 accept
			sockaddr_in _clientAddr = {};
			int cliendAddrLen = sizeof(_clientAddr);
			SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端

			_clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen);//当客户端接入时 会得到连入客户端的socket地址和长度
			if (INVALID_SOCKET == _clientSock) //接受到无效接入
			{
				cout << "ERROR: 接受到无效客户端SOCKET..." << endl;
			}
			else
			{
				cout << "新Client加入:" << "socket = " << _clientSock << " IP = " << inet_ntoa(_clientAddr.sin_addr) << endl;  //inet_ntoa 将ip地址转换成可读的字符串
			}
			g_clinets.push_back(_clientSock);
		}

		for (size_t n = 0; n < fdRead.fd_count; n++)
		{
			if (processor(fdRead.fd_array[n]) == -1)//processor函数是处理命令的逻辑 recv接到的数据并做出相应的判断和输出日志
			{
				auto it = find(g_clinets.begin(), g_clinets.end(), fdRead.fd_array[n]);
				if(it != g_clinets.end())
					g_clinets.erase(it);
			}
		}	
	}

processor()函数就是处理客户端发送数据(本程序也就是处理相关命令login/logout)的逻辑业务,这里不展开介绍。

详细介绍一下服务端的调试中间过程。
1、在select语句设置断点 由于select函数的最后一个参数 阻塞时间设置为NULL,因此这里的select是阻塞调用,也就是当服务端没有需求等待处理的时候,会进行阻塞等待连接/读写操作
在这里插入图片描述
fdRead中有一个socket,是服务端的socket,通过FD_SET(_sock, &fdRead)设置进去的。
此时,服务端程序阻塞。然后打开一个客户端程序,客户端连接成功。在这里插入图片描述
程序阻塞结束,向下运行。这时,会无阻塞的执行accept函数。
总结:通过FD_SET(_sock, &fdRead) 将服务端的socket放入集合中,由于_sock是一个监听函数,当有新客户端连接进来的时候,这个_sock就是可读的。select函数会判断,当前的业务逻辑是有客户端连接,还是有客户端发送了数据。如果是有客户端连接,_sock在select之后还是存在于fdRead中;如果是客户端发送了数据,那么_sock(服务端套接字)就不属于可读的,因为不是连接请求,服务端套接字不需要处理业务逻辑。因此,先将_sock放到fdRead中,再通过select函数进行需求判断。然后再通过if (FD_ISSET(_sock, &fdRead)),判断接下来要处理的逻辑是accept还是recv。

系统是如何判断接下来是想要进行accept,还是进行recv呢。
通过if (FD_ISSET(_sock, &fdRead)),判断服务点的socket是否在fdRead集合中。此时,是有客户端连接,应该进行accept,此时的fdRead中有什么呢?
在这里插入图片描述
可以看到还是服务端的socket自己。因此判断,如果服务端的socket还在fdRead中的时候,是有客户端连接请求需要响应。然后执行accept函数,并将接入的客户端socket保存到动态数组中。

2、客户端发送命令时

		for (size_t n = 0; n < g_clinets.size(); n++)
		{
			FD_SET(g_clinets[n], &fdRead);		//所有连入的客户端放入可读列表 保证recv不阻塞
		}

在进行select之前,将已经连接的socket也放入到fdRead集合中。因为,对于不是服务端的socket,也就是客户端的socket,可读性意味着队列中的数据适合读,当服务端调用recv后不会阻塞。

此时的fdRead数组中有什么呢?(当前只打开了一个客户端程序)
在这里插入图片描述
可以看到有两个Socket,一个是服务端的264,一个是刚连接成功的客户端socket280。程序依旧在select处阻塞,此时在客户端中输入命令login/logout。
程序解除阻塞,重点是,select函数之后,fdRead数组发生了变化。
在这里插入图片描述
服务端的socket被清除了。这也就是为什么if (FD_ISSET(_sock, &fdRead)) 这句代码可以判断,此时的请求是有客户端接入,还是客户端发送了网络报文。
此时,客户端发送了网络报文,fdRead数组中没有服务端的socket,因此对其进行处理。

		for (size_t n = 0; n < fdRead.fd_count; n++)
		{
			if (processor(fdRead.fd_array[n]) == -1)//processor函数是处理命令的逻辑 recv接到的数据并做出相应的判断和输出日志
			{
				auto it = find(g_clinets.begin(), g_clinets.end(), fdRead.fd_array[n]);
				if(it != g_clinets.end())
					g_clinets.erase(it);
			}
		}

猜你喜欢

转载自blog.csdn.net/La745739773/article/details/89054033