网络编程-select模型

1、API详解
(1)int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

        确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。
参数说明:
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

d_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

(2)fd_set类型通过下面四个宏来操作:
fd_set set;
FD_ZERO(&set); /*将set清零,使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd在fdset集合中的状态是否变化返回整型,当检测到fd状态发生变化时返回真,否则,返回假(0)*/

(3)理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
   (a)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
   (b)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
   (c)若再加入fd=2,fd=1,则set变为0001,0011
   (d)执行select(6,&set,0,0,0)阻塞等待
   (e)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。


(4)基于上面的讨论,可以轻松得出select模型的特点:
   (a)可监控的文件描述符个数取决与sizeof(fd_set)的值。
   (b)可以有效突破select可监控的文件描述符上限。
   (c)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。
   (d)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。


2、源码

#include <stdio.h>
#include <winsock.h>   //winsock.h (2种套接字版本)
#include <process.h>   //for beginthread
#pragma comment(lib,"ws2_32.lib") //wsock32.lib 

#define PORT       5150 
#define MSGSIZE     1024 
#define MAX_LISTEN_NUM      5  //最多可同时监听的客户连接数  
#define CONNECT_PORT        1027  
#define CONNECT_IP          "127.0.0.1"  
#define DEFAULT_BUFF_SIZE   512 
#pragma comment(lib, "ws2_32.lib") 
int     g_iTotalConn = 0; 
SOCKET g_CliSocketArr[FD_SETSIZE]; 
DWORD WINAPI WorkerThread(LPVOID lpParameter); 
int main() 
{   
	WSAData     wsdata;  
	SOCKET      sockServer;  
	SOCKET      sockRecvClient;  
	SOCKADDR_IN addrServer;  
	SOCKADDR_IN addrRecvClient;  
	int         addrlenClient;  
	HANDLE      hThread;         //处理客户连接用的线程Handle  
	DWORD        nThreadID;  
	int         nError       = -1;  

	printf(">>>>>>>>server start Startup<<<<<<<<\n");  
	//初始化一个服务进程  
	nError = WSAStartup( MAKEWORD(2,2), &wsdata);  
	if( nError != 0 )  
	{  
		printf("WSAStartup Failed: %d\n",nError);  
		return -1;  
	} 

	//设定本地地址信息  
	addrServer.sin_family = AF_INET;  
	addrServer.sin_port   = htons(CONNECT_PORT);  
	addrServer.sin_addr.S_un.S_addr = inet_addr(CONNECT_IP);  

	// Create listening socket   
	
	sockServer = socket( AF_INET, SOCK_STREAM, 0 );   
	// Bind           
	nError = bind(sockServer, (const sockaddr *)(&addrServer), sizeof(addrServer) ); 

	nError = listen(sockServer, MAX_LISTEN_NUM );   
	  
	// Create worker thread   
	CreateThread(NULL, 0, WorkerThread, NULL, 0, &nThreadID);  

	while (TRUE)   
	{               // Accept a connection     
		addrlenClient   = sizeof( addrRecvClient );  
		sockRecvClient  = accept( sockServer, (sockaddr *)(&addrRecvClient), &addrlenClient );  
		if( sockRecvClient == INVALID_SOCKET )  
		{  
			printf("accept Failed: %d\n",WSAGetLastError());  
			break; //jump while  
		}   
		printf("A accep new client,socketRecvClient = %d\n",sockRecvClient);
		// Add socket to g_CliSocketArr     
		g_CliSocketArr[g_iTotalConn] = sockRecvClient; 
		g_iTotalConn++;
		printf("g_iTotalConn = %d\n",g_iTotalConn);
	}     
	return 0; 
} 


DWORD WINAPI WorkerThread(LPVOID lpParam) 
{   
	int             i;   
	fd_set         fdread;   
	int             ret;   
	struct timeval tv = {1, 0};   
	char           szMessage[MSGSIZE];     
	while (TRUE)   
	{     
		FD_ZERO(&fdread);     
		for (i = 0; i < g_iTotalConn; i++) 
		{ 
			if (0 != g_CliSocketArr[i])//表示socket已经关闭嘞
			{
				FD_SET(g_CliSocketArr[i], &fdread); 
			}		
		} 

		// We only care read event 
		ret = select(0, &fdread, NULL, NULL, &tv); 
		if (ret == 0) 
		{       // Time expired 
			continue; 
		} 
		for (i = 0; i < g_iTotalConn; i++) 
		{ 
			//select函数成功返回时会将未准备好的描述符位清零。
			//使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
			//当描述符fd在描述符集fdset中,返回非零值,否则,返回零。
			if (g_CliSocketArr[i] == 0)  //表示socket已经关闭嘞
			{
				continue;
			}
			if (FD_ISSET(g_CliSocketArr[i], &fdread)) 
			{  
				// A read event happened on g_CliSocketArr 
				printf("A read event happened on g_CliSocketArr g_CliSocketArr[i] = %d\n",g_CliSocketArr[i]);
				ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0); 
			    if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) 
				{ 
					// Client socket closed           
					printf("Client socket %d closed.,ret = %d\n", ret,g_CliSocketArr[i]); 
					closesocket(g_CliSocketArr[i]); 
					FD_CLR(g_CliSocketArr[i],&fdread); //将已经关闭的SOCKET从FD集中删除		
					g_CliSocketArr[i] = 0;

				} 
				else 
				{ 
					// We received a message from client 
					szMessage[ret] = '\0'; 
				///	send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0); 
				} 
			} //if 
		}//for 
	}//while     
	return 0; 
} 


3、问题
(1)为什么会出现select模型?
先看一下下面的这句代码:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返 回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过 你跟踪 一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。 看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。

select模型的出现就是为了解决上述问题。

select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。

(2)Select优势

能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

不用为每一个连接开一个线程。避免线程间过多的切换。

(3)Select不足:

select所用到的FD_SET是有限的,不能同时监听更多。Windows下看的是64,Linux 2.6.15-25-386内核中,该值是1024。

实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值