套接字Select模型使得Windows Sockets应用程序可以在同一时间内管理和控制多个套接字。该模型的核心是select()函数,在使用该函数时,还需要用到FD_SET、FD_ZERO、FD_ISSET和FD_CLR四个宏。
套接字的select模型,能够使得Windows Sockets应用程序同时对多个套接字进行管理。调用select()函数检查当前各个套接字的当前状态。并且根据该函数的返回值,判断套接字的可读可写性。然后调用相应的Windows Sockets API,完成数据的发送和接收等。
Select模型是Windows Sockets中最常见的I/O模型,利用select()函数,应用程序可以判断套接字上是否存在数据,或者是否向该套接字写入数据。
在调用recv()函数接收数据之前,先调用select()函数。如果此时系统没有可读数据,那么select()函数会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序可以调用recv()函数接收数据了。
select()函数原型如下:
int select(
__in int nfds, //忽略,只为了保持和早期Berkeley套接字应用程序的兼容
__inout fd_set *readfds, //具有可读性套接字集合的指针
__inout fd_set *writefds, //具有可写性套接字集合的指针
__inout fd_set *exceptfds, //检查错误套接字集合的指针
__in const struct timeval *timeout //用于设置select()函数时的等待时间
);
该函数返回处于就绪状态并且已经包含在fd_set结构中的套接字总数。如果该函数调用超时则返回0。通过该函数,Windows Sockets应用程序可以判断套接字是否存在数据,或者能否向其写入数据。
1)fd_set结构
该结构是一个管理多个套接字的结构体。在该结构体中,fd_count字段指明套接字的数量,fd_array字段保存fd_count个套接字。fd_set最多可以管理64个套接字。
结构原型如下:
typedef struct fd_set {
u_int fd_count; //套接字数量
SOCKET fd_array[FD_SETSIZE]; //套接字数组
} fd_set;
在程序中使用该结构表示一系列套接字的集合,例如,准备接收数据的套接字集合(可读性集合);准备发送数据的套接字集合(可写性集合)。
当select()函数返回时,会在fd_set结构中返回刚好未完成I/O操作的所有套接字句柄的总量。
readfds参数将包含符合下面任何一个条件的套接字:1)有数据可以读入。此时在该套接字上调用recv()等函数可立即接收到对方的数据;2)连接已经关闭、重设或中止;3)假如已经调用listen()函数,而且一个连接正在建立。那么此时调用accept()函数会成功。
writefds参数将包含符合下面任何一个条件的套接字:1)有数据可以发出。此时在套接字上可以调用send()等函数向对方发送数据;2)如果已经在一个非锁定套接字上调用了connect()函数,且此时连接成功。
exceptfds参数将包含符合下面任何一个条件的套接字:1)如果已经在一个非锁定套接字上调用connect()函数,且此时连接失败;2)有带外(out-of-band,OOB)数据可供读取。
用select()函数对套接字进行监视之前,在应用程序中必须将套接字分配给一个集合。设置可读、可写以及例外的fd_set结构。然后调用select()函数,就可以知道该套接字是否正在发生I/O操作。例如,应用程序欲判断某个套接字是否存在可读数据时,步骤如下:
1)将该套接字加入readfds集合中;2)以readfds为第二个参数调用select()函数;3)当select()函数返回时,应用程序判断该套接字是否仍然存在于readfds集合中;4)如果该套接字存在于readfds集合中,则表明该套接字可读。此时,应用程序调用recv()等函数接收数据。
调用select()函数时,readfds、writefds和exceptfds这3个参数中至少有一个不能设置为NULL。并且,在该非空参数中,必须至少包含一个套接字。否则select()函数将没有任何套接字可以等待。
不管什么原因,加入select()函数调用失败,都会返回SOCKET_ERROR错误代码。
2)timeval结构
该结构用于定义select()函数的等待时间。原型如下:
typedef struct timeval {
long tv_sec; //秒
long tv_usec; //毫秒
} timeval;
调用select()函数时,timeout参数可以取值如下:
1)空指针:select()函数调用会无限期,等到至少有一个套接字符合设置的条件后,该函数返回;2)0:无论是否有套接字符合设置的条件,select()函数都立即返回;3)非0值。
3)宏
Windows Sockets提供了下列宏,可用来针对I/O活动,对fd_set结构进行处理与检查:
FD_CLR(s, *set):从set集合中删除s套接字;
FD_SET(s, *set):将套接字s加入set集合中;
FD_ISSET(s, *set):检查s是否为set集合中的一员,如果是,返回TRUE;
FD_ZERO(*set):将set集合初始化为空集合。
4)select()模型开发步骤
在开发Windows Sockets应用程序时,通过下面步骤完成对套接字的可读可写判断:
1)使用FD_ZERO宏,初始化自己感兴趣的套接字集合fd_set,如FD_ZERO(readfd);
2)使用FD_SET宏,将套接字分配给参与操作的fd_set集合,如FD_SET(s, readfd);
3)以该fd_set为参数调用select()函数。等待在指定的fd_set集合中,I/O活动设置好这个套接字。select()完成后会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4)select()函数成功返回后,使用FD_ISSET宏,对每个fd_set集合进行检查。如FD_ISSET(s, readfd),如果返回值TRUE,则说明这个套接字可读。
5)调用相应的Windows Sockets API进行数据的接收和发送。
select()函数返回后,会修改每个fd_set结构,删除那些不存在的没有完成的I/O操作的套接字。这也是第四步中使用FD_ISSET宏来判断一个套接字是否仍在集合中的原因。
下面的示例是一个服务器程序使用select模型管理套接字的方法,此前服务器listenSocket套接字处于监听状态,准备接受客户端的连接请求。
SOCKET listenSocket; //服务器监听套接字
SOCKET acceptSocket; //接受客户端连接请求套接字
FD_SET socketSet; //服务器套接字集合
FD_SET writeSet; //可写套接字集合
FD_SET readSet; //可读套接字集合
FD_ZERO(&socketSet); //清空服务器套接字集合
FD_SET(listenSocket, &socketSet); //加入监听套接字
while(TRUE)
{
FD_ZERO(&readSet); //清空可读套接字集合
FD_ZERO(&writeSet); //清空可写套接字集合
readSet = socketSet;
writeSet = socketSet;
//调用检查套接字状态
if(SOCKET_ERROR == select(0, &readSet, &writeSet, NULL, NULL))
{
//select函数调用失败
printf("select() returned with error %d/n", WSAGetLastError());
return;
}
//检查是否存在客户端的连接请求
if(FD_ISSET(listenSocket, &readSet))
{
//接受客户端请求
if(INVALID_SOCKET != (acceptSocket=accept(listenSocket, NULL, NULL)))
{
FD_SET(acceptSocket, &socketSet); //将该套接字加入服务器套接字集合
}
else
{
printf("accept() failed with error %d/n", WSAGetLastError());
return;
}
}
//遍历所有的套接字
for(int i=0; i<socketSet.fd_count; i++)
{
SOCKET sAccept = socketSet.fd_array[i];
if(FD_ISSET(sAccept, &readSet)) //该套接字可读
{
//调用输入函数,接收数据
}
if(FD_ISSET(sAccept, &writeSet)) //该套接字可写
{
//调用输出函数,发送数据
}
}
}
优势与不足:
Select模型优势在于可以同时对多个建立起来的套接字进行有序的管理。可以防止应用程序在一次I/O调用过程中,使阻塞模式套接字被迫进入阻塞状态;使非阻塞套接字产生WSAEWOULDBLOCK错误。
select()函数就好像一个消息中心,当消息到来时,通知应用程序接收和发送数据。这使得Windows Sockets应用程序开发人员可以把精力更多地集中在如何处理数据的发送和接收上。
我们可以看到,完成一次I/O操作经历了两次Windows Sockets函数的调用。例如,当接收对方数据时,第一步,调用select()函数等待该套接字的满足条件;第二步,调用recv()函数接收数据。这种结果与在一个阻塞模式的套接字上调用recv()函数是一样的,因此,使用select()函数的Windows Sockets程序,其效率可能受损。因为每一个Windows Sockets I/O调用都会经过该函数,因而会导致严重的CPU额外负担。