本文部分截取他人博客,自己总结完成,附带各自链接:
http://blog.csdn.net/yunboy4/archive/2009/09/18/4566124.aspx
http://dev.csdn.net/article/82132.shtm
http://uestczly.blog.163.com/blog/static/111354231200924105116788/
http://m.cnblogs.com/32508/1089271.html
先看一下下面的这句代码:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差(套接字很多的话,线程也必然很多,线程切换浪费很多时间)。Select模型就是为了解决这个问题而出现的。
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型就是用来解决上述问题而设计的。
select(选择)模型是winsock中常见的I/O模型。之所以称其为“select模型”,是由于它的“中心思想”是利用select函数,实现对I/O的管理!最初设计该模型时,主要面向的是某些使用Unix操作系统的计算机,它们采用的是Berkeley套接字方案。select模型已经集成到Winsock1.1中。
该函数定义如下:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
n:该参数忽略,之所以保留是因为和早期的berkeley函数兼容
readfds: 该参数为用于检查是否有数据需要读入(接收)的集合
writefds:该参数为用于检查是否有数据需要写出(发送)的集合
exceptfds:该参数为用于检查是否有"例外数据"需要读取的集合.
timeout:设置select函数检查的超时时间,如果设为NULL,则无限制等待直到上述集合有改变
为止,如果设置为0,则select函数立即返回。
需要注意的是,上述三个集合之中,至少一个不为NULL,不为NULL的集合里,至少一个不为空,否则函数就没有等待的目标而导致该函数失去作用。
其中fd_set为WINSOCK提供的一个文件集描述符(简单的说就是一个集合而已),里面能够容纳数个SOCKET句柄。这就不得不提到select模型的另外几个核心的函数.
FD_SET(Socket s,fd_set *SET) //将s句柄放入集合SET
FD_ZERO(fd_set *SET) //清空SET
FD_CLR(Socket s,fd_set *SET) // 从SET中清除s
FD_ISSET(Socket s,fd_set *SET) //检查s是否在集合SET内,是返回TRUE,否返回FALSE
select函数有个很重要的特性,就是会自动删除相应集合里不符合要求的SOCKET句柄,例如,SOCKET S作为需要检查的对象,用FD_SET放入readfds(当然readfds是自己申明的),然后用select函数进行检查,设置timeout为10s,那么如果10s内S没有需要读入的数据,select函数返回的时候会自动将S从readfds中删除,那么在用FD_ISSET检查的时候便可以分辨哪些是可读可写的。
一般程序大致走向如下。
1. SOCKET相关操作(create,bind,listen,accept……)
2. 建立readfds,writefds等集合
3. FD_ZERO初始化集合
4. FD_SET将SOCKET句柄放入感兴趣的集合内
5. 适时调用select
6. FD_ISSET检查感兴趣集合
7. 返回 3(一般都是有这一步)
如果select函数每次会自动删除不符合要求的S,那如果采取顺序的执行方式肯定不行了,为什么?因为S如果此时没有数据读入,则从readfds中删除了,那么如果下次仍然需要检查,又必须要调用FD_SET函数才行,这样很自然的就形成了一个循环。虽然MICROSOFT的教科书上出于对程序性能、效率的考虑不建议使用轮询的方式(即循环)进行检查,但是如果该应用对实时有一定要求,而且会多次发送数据,却避免不了,而且存在一个严重的问题,如果在一个线程里实现的话,FD_ISSET必须要得按顺序检查集合,这就导致了程序可能必须得先读,然后再写或者完全相反(这样也就不符合逻辑了)。
最后讨论select模型的优缺点,从一个可以使用的应用程序来看,select模型几乎没有什么价值,唯一的优点便是模型十分简单,容易理解,可以一次提供对多个套接字的服务(虽然不太适合),从某种程度上避免了多线程(其实还不如使用阻塞函数的多线程效果好(摘抄--还需要研究,不是很理解)),过程十分清晰,其最直接的一点便是避免了程序因为同步的问题卡死。缺点的话用一句概括就是几乎没有实用价值,有时为了达到实时不可避免的要使用轮询来支持实时通信。
总之,select模型作为异步IO模型的一份子,还是有它的意义,凡事从易到难,select模型应该是个不错的开始。
select中限制了连接socket的数目,不过可以通过手工改动,改变socket的数目,最好不要大于1024
//winsock2.h中的定义
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif /* FD_SETSIZE */
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
示例代码:
int main(int argc, char* argv[])
{
WSADATA wsaData;
WORD sockVersion=MAKEWORD(2,0);
WSAStartup(sockVersion,&wsaData);
SOCKET clientsockarray[FD_SETSIZE - 1];
SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(s==INVALID_SOCKET)
{
printf(" Failed socket()/n");
WSACleanup();
return 0;
}
sockaddr_in sin;
sin.sin_family=AF_INET;
sin.sin_port=htons(8888);
sin.sin_addr.S_un.S_addr=INADDR_ANY;
if(bind(s,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR)
{
printf(" Failed bind() %d/n",GetLastError());
WSACleanup();
return 0;
}
if(listen(s,63)==SOCKET_ERROR)
{
printf(" Failed listen()/n");
WSACleanup();
return 0;
}
for (int i=0;i < 64; i++)
{
clientsockarray[i] = INVALID_SOCKET;
}
sockaddr_in remoteAddr;
int nAddrLen=sizeof(remoteAddr);
//char szText[]="Hello World!";
FD_SET fd;
FD_ZERO(&fd);
TIMEVAL tv={30,0};
FD_SET(s,&fd);
printf("server start:%d/n",GetLastError());
int num=0;
while(TRUE)
{
int nResult;
nResult = select(0,&fd, NULL,NULL,&tv);
if (nResult == SOCKET_ERROR)
{
printf("select failed /n");
continue;
}
/*
if (nResult > 0)
{
printf("dddddddddd/n");
}
*/
if(FD_ISSET(s,&fd))
{
SOCKET client;
client=accept(s,(SOCKADDR*)&remoteAddr,&nAddrLen);
if (client == INVALID_SOCKET)
{
printf("client connect fail %d",GetLastError());
continue;
}
if(!InsertSock(clientsockarray,client))
{
printf("³¬¹ý×î´óSOCKET/n");
closesocket(client);
continue;
}
FD_SET(client,&fd);
}
nResult = select(0,&fd, NULL,NULL,&tv);
if (nResult == SOCKET_ERROR)
{
printf("select failed /n");
continue;
}
for (int nIndex = 0; nIndex <= FD_SETSIZE - 1; nIndex++)
{
f(FD_ISSET(clientsockarray[nIndex], &fd))
{
//printf("aaaa/n");
char buffer[256];
int nRet = recv(clientsockarray[nIndex], buffer, sizeof(buffer), 0);
if (nRet == 0 || nRet == SOCKET_ERROR)
{
closesocket(clientsockarray[nIndex]);
clientsockarray[nIndex] = INVALID_SOCKET;
continue;
}
printf("has recv data from socket %d : %s",i, buffer);
printf(" num:%d/n",++num);
}
}
}
closesocket(s);
WSACleanup();
return 0;
}
示例代码2:read、write集都有
while(1)
{
FD_ZERO(&ReadSockFd);
FD_ZERO(&WriteSockFd);
ReadSockFd = AllSockFd;
WriteSockFd = AllSockFd;
int nRet = select(0, &ReadSockFd, &WriteSockFd, NULL, NULL);
if(SOCKET_ERROR == nRet)
{
continue;
}
//有请求事件发生
if (FD_ISSET(ListenSock, &ReadSockFd))
{
//接受请求
SOCKET ClientSock;
u_short Port;
bool nRe = (*(Pam.pListenSock)).Accept(&ClientSock, 0, &Port);
if(nRe)
{
FD_SET(ClientSock, Pam.pClientSockFd);
//设置套接字发送缓冲区80K
int nBuf = SOCKET_BUFF;
int nBufLen = sizeof(nBuf);
int nRe = setsockopt(ClientSock, SOL_SOCKET, SO_SNDBUF, (char*)&nBuf, nBufLen);
if(SOCKET_ERROR == nRe)
AfxMessageBox("setsockopt error!");
//检查缓冲区是否设置成功
nRe = getsockopt(ClientSock, SOL_SOCKET, SO_SNDBUF, (char*)&nBuf, &nBufLen);
if(SOCKET_BUFF != nBuf)
AfxMessageBox("检查缓冲区:setsockopt error!");
else
AfxMessageBox("已连接客户端!");
}
}
//判断是否可读或可写
for(u_int n = 0;n < ClientSockFd.fd_count;n++)
{
if(FD_ISSET(ClientSockFd.fd_array[n], &ReadSockFd)) //发现可读
{
//接收数据
//如果失败 删除此元素
}
if(FD_ISSET(ClientSockFd.fd_array[n], &WriteSockFd)) //发现可写
{
//发送缓冲区未满可以发送
//如果失败 删除此元素
}
}
}