1WinsockI/O方法设计、讲授设计、讲授:谭献海EmailEmail::xhtan@home.swjtu.edu.cn2009年10月2主要内容1、套接字模式z1.1阻塞模式z1.2非阻塞模式2、套接字I/O模型z2.1select模型z2.2WSAAsyncSelectz2.3WSAEventSelectz2.4重叠(overlapped)模型z2.5完成端口模型3、I/O模型的问题4、小结3Winsock分别提供了“套接字模式”和“套接字I/O模型”,来对一个套接字上的I/O行为加以控制。其中,套接字模式用于决定在随一个套接字调用时,该Winsock函数的行为。而套接字I/O模型描述了一个应用程序如何对套接字上的I/O进行管理及处理。套接字模式:z阻塞(Blocking)z非阻塞(non-blocking)套接字I/O模型:zSelect(选择)zWSAAsyncSelect(异步选择)zWSAEventSelect(事件选择)zOverlappedI/O(重叠式I/O)zCompletionport(完成端口)4操作系统对套接字I/O模型的支持情况5性能测试结果下表是NetworkProgrammingforMicrosoftWindows2nd一书中对不同模式的一个性能测试结果。服务器采用Pentium41.7GHzXeon的CPU,768M内存;客户端有3台PC,配置分别是Pentium2233MHz,128MB内存,Pentium2350MHz,128MB内存,Itanium733MHz,1GB内存。61套接字模式Windows套接字在两种模式下执行I/O操作:阻塞和非阻塞。z在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序),直到该函数操作完成,或出错。z在非阻塞模式下,Winsock函数无论如何都会立即返回。71.1阻塞模式对于处在阻塞模式的套接字,我们必须多加留意,因为在一个阻塞套接字上调用任何一个WinsockAPI函数,都会产生相同的后果—耗费或长或短的时间“等待”。一个典型的例子8简单的阻塞模式示例SOCKETsock;charbuff[256];intdone=0;……while(!done){nBytes=recv(sock,buff,65,0);if(nBytes==SOCKET_ERROR){printf(“recvfailedwitherror%d\n”,WSAGetLastError());return;}DoComputationData(buff);}……代码的问题:假如没有数据处于“待决”状态,那么recv函数可能永远都无法返回。只有从系统的输入缓冲区中读回点什么东西,才返回!9解决办法之一:在recv中使用MSG_PEEK标志,或者调用ioctlsocket(设置FIONREAD选项),在系统的缓冲区中,事先“偷看”是否存在足够的字节数量。但在“偷看”的时候,对系统造成的开销是极大的。应尽量避免措施一:将应用程序划分为一个读线程,和一个计算线程。两个线程都共享同一个数据缓冲区。用一个同步对象,比如一个事件或者Mutex(互斥体)进行进程之间的同步。“读线程”的职责是从网络连续地读入数据,并将其置入共享缓冲区内。读线程将计算线程开始工作至少需要的数据量拿到手后,便会触发一个事件,通知计算线程从缓冲区取去数据,进行要求的计算。10多线程的阻塞套接字示例分别提供了两个函数,一个负责读取网络数据(ReadThread),另一个则负责对数据执行计算(ProcessThread)。读线程定义临界区对象定义事件对象11设置信号,通知计算线程进入临界区离开临界区12多线程方法尽管会增大一些开销,但的确是一种可行的处理阻塞套接字方案。唯一的缺点便是扩展性极差,难以处理大量套接字。计算线程等待读线程信号进入临界区离开临界区131.2非阻塞模式非阻塞模式的套接字在使用上稍显困难,但它在功能和效率上要强大得多。创建一个套接字,并将其置为非阻塞模式的程序示例:SOCKETs;Unsignedlongul=1;intnRet;s=socket(AF_INET,SOCK_STREAM,0);nRet=inctlsocket(s,FIOBIO,(unsignedlong*)&ul);if(nRet==SOCKET_ERROR){//Failedtoputthesocketintononblockingmode}14设置一个非阻塞套接字示例将一个套接字置为非阻塞模式之后,WinsockAPI调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个WSAEWOULDBLOCK错误。例如在系统的输入缓冲区中,尚不存在“待决”的数据时,recv(接收数据)调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。非阻塞套接字上的WSAEWOULDBLOCK错误15阻塞和非阻塞套接字模式各存在着优点和缺点。从概念的角度说,阻塞套接字更易使用。但在应付建立连接的多个套接字时,或在数据的收发量不均,时间不定时,却显得极难管理。而另一方面,由于需要编写更多的代码,以便在每个Winsock调用中,对收到一个WSAEWOULDBLOCK错误的可能性加以应付,那么非阻塞套接字便显得有些难于操作。在这些情况下,可考虑使用“套接字I/O模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上进行的通信事件加以管理。162套接字I/O模型五种类型的套接字I/O模型,可让Winsock应用程序对I/O进行管理。zselect模型(选择)zWSAAsyncSelect模型(异步选择)zWSAEventSelect模型(事件选择)zoverlapped模型(重叠)zcompletionport模型(完成端口)172.1select模型select()可以提供类似windows中的消息驱动机制,实现对I/O的管理。通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否有接收数据,或者能否向一个套接字写入数据,或者出现意外。目的是防止应用程序在套接字处于阻塞模式中时,在一次I/O绑定调用(如send或recv)过程中,被迫进入“阻塞”状态;同时防止在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数会在进行I/O操作时阻塞。select的函数原型如下:#includesys/time.h#includesys/types.h#includeunistd.hintselect(intnfds,fd_set*readfds,fd_set*writefds,fd_est*exceptfds,structtimeval*timeout);18说明:z函数失败的返回值:调用失败返回SOCKET_ERROR,超时返回0。zreadfds、writefds、exceptfds三个变量至少有一个不为空,同时这个不为空的套接字组中至少有一个socket。参数:z参数nfds会被忽略。提供这个参数主要是为了保持与早期的Berkeley套接字应用程序的兼容。z参数readfds用于检查可读性,readfds集合包括符合下述任何一个条件的套接字:z有数据可以读入。z连接已经关闭、重设(Reset0或中止)。z假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。19z参数writefds用于写数据,writefds集合包括符合下述任何一个条件的套接字:z有数据可以发出。z如果已完成了对一个非阻塞连接调用的处理,连接就会成功。z参数exceptfds用于例外数据,exceptfds集合包括符合下述任何一个条件的套接字:z假如已完成了对一个非阻塞连接调用的处理,连接尝试失败。z有带外(Out-of-band,OOB)数据可供读取。z参数timeout对应的是一个时间指针,它指向一个timeval结构,用于决定select最多等待I/O操作完成多久的时间。20timeval结构structtimeval{longtv_sec;/*seconds*/longtv_usec;/*microseconds*/};参数:ztv_sec:以秒为单位指定等待时间;ztv_usec:以毫秒为单位指定等待时间说明:此结构主要是设置select()函数的等待值,如果将该结构设置为(0,0),则select()函数会立即返回。21fd_set结构#defineFD_SETSIZE64typedefstructfd_set{u_intfd_count;/*howmanyareSET?*/SOCKETfd_array[FD_SETSIZE];/*anarrayofSOCKETs*/}fd_set;参数:fd_count:已设定socket的数量fd_array:socket列表,FD_SETSIZE为最大socket数量,建议不小于64(微软建议)。22用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部读、写以及例外fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。Winsock提供了下列宏操作,可用来针对I/O活动,对fd_set进行处理与检查:zFD_CLR(s,*set):从set中删除套接字s。zFD_ISSET(s,*set):检查s是否是set集合的一名成员zFD_SET(s,*set):将套接字s加入集合set。zFD_ZERO(*set):将set初始化成空集合。23用select操作一个或多个套接字句柄的全过程1.使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。2.使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。3.调用select函数,然后等待在指定的fd_set集合中,I/O活动设置好一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。4.根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决)的I/O操作—具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。5.知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤1),继续进行select处理。24例子:用select管理一个套接字上的I/O操作procedureTListenThread.Execute;varaddr:TSockAddrIn;fd_read:TFDSet;timeout:TTimeVal;ASock,MainSock:TSocket;len,i:Integer;beginMainSock:=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);addr.sin_family:=AF_INET;addr.sin_port:=htons(5678);addr.sin_addr.S_addr:=htonl(INADDR_ANY);bind(MainSock,@addr,sizeof(addr));listen(MainSock,5);25while(notTerminated)dobeginFD_ZERO(fd_read);FD_SET(MainSock,fd_read);timeout.tv_sec:=0;timeout.tv_usec:=500;ifselect(0,@fd_read,nil,nil,@timeout)0then//至少有1个等待Accept的connectionbeginifFD_ISSET(MainSock,fd_read)thenbeginfori:=0tofd_read.fd_cou