在套接字创建时,默认工作在阻塞模式下,例如对Recv函数的调用会使程序进入等待状态,直到接收到数据才返回。大多数WinSock程序设计中都是从阻塞套接字模式开始学习的。因为这事最容易和最直接的方式。处理阻塞模式套接字的应用程序使用的程序框架便是阻塞模型。阻塞套接字的好处是使用简单。但是当需要处理多个套接字连接时,就必须创建多个线程。即典型的一个连接使用一个线程的问题。这给编程带来了许多不便,所以实际开发中使用最多的还是非阻塞模式。
非阻塞模式使用起来比较复杂,但是有许多优点,应用程序可以调用ioctlsocket函数显示地让套接字工作在非阻塞模式下,如下代码所示:
u_long ul=1;
SOCKET s=socket(AF_INET,SOCK_STREAM,0);
ioctlsocket(s,FIONBIO,(u_long*)&ul);
一旦套接字被处于非阻塞模式,处理发送和接收数据或者管理连接的Winsock调用将会立即返回。大多数情况下,调用失败的出错代码是WSAEWOULDBLOCK,这意味着请求的操作在调用期间没有完成。例如,如果系统输入缓冲区没有待处理的数据,那么对recv的调用将返回WSAEWOULDBLOCK.通常要对相同函数调用多次,知道它返回成功为止。
非阻塞调用经常以WSAEWOULDBLOCK出错代码失败,所以将套接字设置为非阻塞之后,问题的关键在于如何确定套接字什么时候可读/可写,也就是说确定网络时间何时发生。如果需要自己不断调用函数去测试的话,程序的性能势必受到影响,解决的方法就是使用Windows提供的不同的I/O模型。
1选择模型:
选择模型是一个广泛在Winsock中使用的i/o模型,称它为选择模型,是因为它主要是使用select函数来管理i/o。这个模式的设计源于UNIX系统,目的是允许那些想要避免套接字调用上阻塞的应用程序有能力管理多个套接字。
select函数:
select函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件的发生,便进入等待状态,以便执行同步I/o。函数定义如下:
int select
{
int nfds,//忽略。仅是为了与berkeley套接字兼容
fd_set* readfds,//指向一个套接字集合,用来检查其可读性
fd_set* writefds,//指向一个套接字集合,用来检查其可写性
fd_set* exceptfds,//指向一个套接字集合,用来检查错误
const struct timeval* timeout //指定此函数等待的最长时间,如果为NULL,则最长时间为无限大
};
The select function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.
int select( __in int nfds, __in_out fd_set* readfds, __in_out fd_set* writefds, __in_out fd_set* exceptfds, __in const struct timeval* timeout );
Parameters
-
nfds
-
Ignored. The nfds parameter is included only for compatibility with Berkeley sockets.
readfds
-
Optional pointer to a set of sockets to be checked for readability.
writefds
-
Optional pointer to a set of sockets to be checked for writability.
exceptfds
-
Optional pointer to a set of sockets to be checked for errors.
timeout
-
Maximum time for select to wait, provided in the form of a TIMEVAL structure. Set the timeout parameter to null for blocking operations.
2 套接字集合
fd_set结构可以把多个套接字连在一起,形成了一个套接字集合。select函数可以测试这个集合中哪些套接字有事件发生,下面是这个结构在WINSOCK2.h中的定义:
fd_set Structure
The fd_set structure is used by various Windows Sockets functions and service providers, such as theselect function, to place sockets into a "set" for various purposes, such as testing a given socket for readability using thereadfds parameter of theselect function.
typedef struct fd_set { u_int fd_count; //下面数组的大小 SOCKET fd_array[FD_SETSIZE];//套接字句柄数组 } fd_set;//定义结构体时,前面加了typedef,则定义结构体对象不需要加struct
下面是WINSOCK定义的4个操作fd_set套接字集合的宏:
(1) FD_ZERO(*set) //初始化set为空集合,集合在使用前应该总是清空。
(2) FD_CLR(s,*set) //从set集合中移除套接字s
(3) FD_ISSET(s,*set) //检查s是不是set的成员,如果是返回TRUE。
(4) FD_SET(s,*set) //添加套接字s到集合
2 网络事件
传递给select函数的3个fd_set结构中,一个是为了检查可读性(readfds),一个是检查可读性(writefds),另一个是为了检查错误(exceptfds)。
select函数返回之后,如果有下列事件发生,其对应的套接字就会被标识。
(1)readfds集合:
1‘ 数据可读 2’ 连接已经关闭、重启或者中断 3‘ 如果listen已经被调用,并且有一个连接未决,accept函数将成功。
(2) writefds集合:
1’ 数据能够传送 2‘ 如果一个非阻塞连接调用正在被处理,连接已经成功
(3)exceptfds集合:
1’ 如果一个非阻塞连接调用正在被处理,连接试图失败。 2‘ OOB数据可读
当select返回时,它通过移除没有未决i/o操作的套接字句柄修改每个fd_set集合。例如,想要测试套接字s是否可读时,必须将它添加到readfds集合,然后等待select函数返回。当select调用完成后再确定s是否依然还在readfds集合中,如果还在就说明s可读,3个参数中的任意两个都可以是NULL(至少要有一个不是NULL),任何不是NULL的集合必须至少包含一个套接字句柄。
3 设置超时
最后的参数timeout是timeval结构的指针,它指定了select函数等待的最长时间。如果设为NULL,select将会无限阻塞,直到有网络事件发生,timeval结构定义如下:
typedef struct timeval
{
long tv_sec;//指示等待多少秒
long tv_usev;//指示等待多少毫秒
}timeval;
The timeval structure is used to specify time values. It is associated with the Berkeley Software Distribution (BSD)Time.h header file.
typedef struct timeval { long tv_sec; long tv_usec; } timeval;
Members
-
tv_sec
-
Time value, in seconds.
tv_usec
-
Time value, in microseconds.
应用举例:
程序运行之后,在4567端口监听,接受客户端连接请求,打印出接收到得数据。大家可以看到采用select模型之后,即便是在单个线程中,也可以管理多个套接字。具体编程流程如下:
(1)初始化套接字集合fdSocket,向这个集合添加监听套接字句柄。
(2)将fdSocket集合的拷贝fdRead传递给select函数。当有事件发生时,select函数移除fdRead集合中没有未决i/o操作的套接字句柄,然后返回。
(3) 比较原来fdSocket集合与select处理过的fdRead集合,确定哪些套接字有未决i/o,为进一步处理这些i/o。
(4)回到第二步继续进行选择处理
代码如下:
int main()
{
USHORT nPort=4567;//此服务器监听的端口号
//创建监听套接字
SOCKET sListen =::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
sockaddr_in sin;
sin.somfamily =AF_INET;
sin.sin_port=htons(nPort);
sin.sin_addr.S_un.S_addr=INADDR_ANY;
//绑定套接字到本地机器
if(::bind(sListen,(sockaddr*)&sin,sizeof(sin))==SOCKET_ERROR)
{
printf("Failed bind()\n");
return -1;
}
//进入监听模式
::listen(sListen,5);
//select模型处理过程
//初始化一个套接字集合fdSocket,添加监听套接字句柄到这个集合
fd_set fdSocket; //所以可用套接字集合
FD_ZERO(&fdSocket);
FD_SET(sListen,&fdSocket);
while(TRUE)
{
//将fdSocket集合的一个拷贝fdRead传递给select函数,当有事件发生时,select函数移除fdRead集合中没有未决i/o操作的套接字句柄,然后返回
fd_set fdRead =fdSocket;
int nRet =::select(0,&fdRead,NULL,NULL,NULL);
if(nRet>0)
{
//通过将原来fdSocket集合与select处理过的fdRead集合比较,确定都有哪些套接字有未决i/o,并进一步处理这些i/o
for(int i=0;i<(int)fdSocket.fd_count;i++)
{
if(FD_ISSET(fdSocket.fd_array[i],&fdRead))
{
if(fdSocket.fd_array[i] ==sListen)//监听套接字接收到新连接
{
if(fdSocket.fd_count<FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen =sizeof(addrRemote);
SOCKET sNew=::accept(sListen,(SOCKADDR*)&addrRemote,&nAddrLen);
FD_SET(sNew,&fdSocket);
printf("接收到连接(%s)\n"),::inet_ntoa(addrRemote.sin_addr);
}
else
{
printf("Too much Connection!\n");
continue;
}
}
else
{
char szText[256];
int nRecv=::recv(fdSocket,fd_array[i],szText,strlen(szText),0);
if(nRecv>0)
{
szText[nRecv]=’\0‘;
printf("接收到数据:%s\n",szText);
}
else
{
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i],&fdSocket);
}
}// 如果监听套接字接收到连接,则进行等待客户端的请求,调用accept()函数。否则,调用可读套接字中其他的套接字进行接收数据。
}
}
}
else
{
printf("Failed select()\n");
break;
}
}
return 0;
}
使用select的好处是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是,添加到fd_set结构的套接字数量是有限制的,默认情况下,最大值为FD_SETSIZE,它在winsock2.h未见中定义为64。为了增加套接字数量,应用程序可定义的值也不能超过Winsock下层提供者的限制(通常为1024).
另外,FD_SETSIZE值太大的话,服务器性能就会受到影响,例如,有1000个套接字,那么在调用selec之前就不得不设置这1000个套接字,select返回之后,又必须坚持这1000个套接字。