winsock中的I/O模型

来源:百度文库 编辑:神马文学网 时间:2024/04/17 06:01:26
         socket为了实现非阻塞模式,winsock提供了几种不同的套扫字I/O模型对I/O进行管理,包括:select(选择),WSAAsyncSelect(异步选择),WSAEventSelect(事件选择),Overlapped(重叠)以及 Completion port(完成端口),    另可以用ioctlsocket(SOCKET s, FIOBIO, int &cmd )设置非阻塞模式,不过这样会非常的复杂。
         select是winsock中最常见的i/o模型。通过调用select函数可确定一个或多个套接字的状态,判断套接字上是否存在数据,或都能否向一个套接字写入数据。它既能防止应用程序在套接字处于阻塞模式时,在一次i/o操作后被阻塞,同时也防止在套接字处于非锁定模式中时,产生WSAEWOULDBLOCK错误。
1.select函数
         int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout );
其中,参数nfds可忽略,仅仅起到与Berkeley套接字兼容的作用。 readfds,writefds,exceptfds三个fd_set数据类型的参数分别为指向等待可读性检查的套接字组,等待可写性检查的套接字组和指向等待错误检查的套接字组的指针。在这三个fd_set参数中,至少有一个不为NULL,在任何不为空的集合中,必须包含至少一个套接字句柄,否则,select()函数便没有任何东西可以等待了。参数timeout为一timeval结构数据,用于指定select()最多等待时间,对阻塞操作则为NULL。timeval结构的格式为:
struct timeval
{
 long tv_sec; //秒
 long tv_usec; //毫秒
}
         其中,tv_sec字段以秒为单位指定等待时间。tv_usec字段则以毫秒为单位指定等待时间。若将超时值设置为(0,0),则select()会立即返回,允许应用程序对select操作进行"轮询"。出于对性能方面的考虑,应避免这样的设置。select()调用成功返回时,fd_set结构中将存有满足条件的套接字组的子集,并且select()返回满足条件的套接字的数目。若调用超过timeval设定的时间,便会返回0;若调用失败,则返回SOCKET_ERROR。
         winsock提供了FD_SETSIZE变量用于确定一个集合中最多的套接字描述字数目(FD_SETSIZE缺省值为64,可在包含winsock.h前用 #define FD_SETSIZE 来改变该值)。此外,还提供了四个宏对fd_set结构进行操作,它们分别为:
   FD_CLR( s, *set );从集合set中 删除套接字句柄s
   FD_ISSET( s, *set ); 若s为集合中一员,非零;否则为零。
   FD_SET( s, *set );向集合添加套接字句柄s
   FD_ZERO( s, *set ); 将set初始化为空
若想测试一个套接字是否"可读",则操作如下:
 1.将该套接字增加到readfd集合中:
 fd_set fdread;
 FD_ZERO(&fdread);
 FD_SET( s, &fdread );
 2.调用select()函数:
 select( 0, &fdread, NULL, NULL, NULL );
 3.等待select()返回,若调用成功,则判断该套接字是否为集合一员,若答案是肯定的,便表明该套接字“可读”,可立即从它上面读取数据:
 if( FD_ISSET( s, &fdread )
 {
  //从套接字中读取数据
 }


2.WSAAsyncSelect模型
         WSAAsyncSelect模型也是一个常用的异步I/O模型,利用这个模型,应用程序可在一个套接字上接收以windows消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsyncSelect函数自动将套接字设置为非阻塞模式,并向winsock dll注册一个或多个感兴趣的网络事件,并提供一个通知时使用的窗口句柄,当注册的网络事件发生时,对应窗口将收到一个基于消息的通知。WSAAsyncSelect函数的原型为:
         int WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );
         其中,字段s用于标识一个需要事件通知的套接字的句柄。hWnd标识一个在网络事件发生时需要接收消息的窗口句柄。wMsg在网络事件发生时指定窗口要接收到的消息,它为一个自定义的消息,最后一个参数是lEvent,为位屏蔽码,用于指明应用程序感兴趣的网络事件集合。它可以为以下值:
FD_READ  欲接收读准备好的通知
FD_WRITE 欲接收写准备好的通知
FD_OOB  欲接收带边数据到达的通知
FD_ACCEPT 欲接收写准备好的通知
FD_CONNECT 欲接收已连接好的通知
FD_CLOSE 欲接收套接字关闭的通知
         对不同的事件区分不同的消息是不可能的,困此下面的代码将不会工作,第二个调用将会使第一次调用的作用失效,只有FD_WRITE会通过wMsg2消息通知到,也正困为如此,多个事件必须在套接字上一次注册:
rc = WSAAsyncSelect( s, hWnd, wMsg1, FD_READ );
rc = WSAAsyncSelect( s, hWnd, wMsg2, FD_WRITE );
         如果要取消所有的通知,即winsock的实现不再在套接字上发送任何和网络事件相关的消息,则应将lEvent参数置为0:
WSAAsyncSelect( s, hWnd, 0, 0 );
         这样,WSAAsyncSelect立即使传给该套接字的事件消息无效,但仍有可能有消息等在应用程序的消息队列中,应用程序困此也必须准备好接收网络消息,即使消息作废。用closesocket关闭一个套接字也同样使WSAAsyncSelect发送的消息作废,但在closesocket这前队列中的消息仍然起作用。
         由于一个已调用accept的套接字和用来接收它的侦听套接字有同样的属性,任何为侦听套接字设置的WSAAsyncSelect事件也同样对已接收的套接字起作用。例如,如果一个侦听套接字有WSAAsyncSelect事件FD_ACCEPT,FD_READ,FD_WRITE,则任何在那个侦听 的套接字上接收的套接字将也有FD_ACCEPT,FD_READ,FD_WRITE事件,以及同样的wMsg的值。若需要不同的sMsg及事件,应用程序应调用WSAAsyncSelect,将已接收的套接字和想要发送的新消息作为参数传递。
         当某一套接字s上发生了一个已注册的网络事件,应用程序窗口hWnd会接收到消息wMsg.wParam参数标识了网络事件发生的套接字,lParam的低字指明了发生的网络事件,lParam的高字则含有一个错误代码,该错误代码可以是winsock.h中声明的任何错误。
****应用程序如果需要给侦听的和调用 过accept()的套接字以不同的wMsg,它就应该在侦听的套接字上请求FD_ACCEPT事件,然后在accept()调用后设置相应的事件。由于FD_ACCEPT从不发送给已连接的套接字,而FD_READ,FD_WRITE,FD_OOB及FD_CLOSE也从不发送给侦听套接字,所以不会产生困难。
若应用程序感兴趣的网络事件注册成功,则返回0,否则返回SOCKET_ERROR.


3.WSAEventSelect模型
          WSAEventSelect模型是winsock提供的另一个有用的异步I/O模型,它和WSAAsynSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知,并且它支持的网络事件与WSAEventselect模型的一样。它与WSAAsyncSelect模型最主要的差别在于网络事件会被发送到一个事件对象句柄,而不是发送到一个窗口。
          首先要调用函数WSACreateEvent()创建事件对象来接收网络事件,该函数原型为:
          WSAEVENT WSACreateEvent(void);
          它的返回值是一个事件对象句柄,该事件具有两种工作状态:"已传信"(signaled)和"未传信"(nosignaled)及两种工作模式:"人工重设"(manual reset)和"自动重设"(auto reset).默认状态下事件处于未传信的工作状态和人工重设模式。
接下来就要调用WSAEventSelect()函数将所创建的事件对象与某个套接字关联在一起,同时注册感兴趣的网络事件类型,使事件对象的工作状态从"未传信"转变成"已传信"。 WSAEventSelect函数原型为:
         int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents );
         其中,参数s为一个标识套接字句柄。hEventObject参数用于指定与所提供的FD_XXX网络事件集合相关的一个事件对象句柄。lNetworkEvents参数是一个屏蔽位,用于指定感兴趣FD_XXX网络事件组合。
    由于事件对象创建后默认处于人工重设模式,所以在完成了一个I/O请求的处理后,应用程序需要调用WSAResetEvent函数将事件对象的工作状态从已传信更改为未传信。
WSAResetEvent函数的原型为:
  BOOL WSAResetEvent(WSAEVENT hEvent );
  该函数唯一的参数是一个事件句柄,成功则返回TRUE,失败返回FALSE。
  一个套接字同一个事件对象句柄关联在一起后,应用程序就可以通过WSAWaitForMultipleEvents函数等待网络事件来触发事件句柄的工作状态,进行I/O处理;WSAWaitForMultipleEvents函数原型为:
  DWORD WSAWaitForMultipleEvents(
   DWORD cEvents,
   const WSAEVENT FAR* lphEvents,
   BOOL fWaitAll,
   DWORD dwTimeout,
   BOOL fAlertable );
      其中,lphEvents参数为指向一个事件对象句柄数组的指针。cEvents参数指出所指向的数组事事件对象句柄的数目,事件对象句柄数组的最大值为WSA_MAXIMUM_WAIT_EVENTS。fWaitAll参数指定等待类型,若为TRUE,则当lphEvents数组中的所有事件对象同时有信号时,函数返回;若为FALSE,则当任意一个事件对象有信号时函数就返回。在后一种情况下,返回值指出是哪一个事件对象造成函数返回。通常把参数设为FALSE,一次只为一个套接字事件提供服务。dwTimeout参数指定超时等待时间(以毫秒计),当超时间隔到,不论fWaitAll参数所指定的条件是否满足,函数即返回。如果dwTimeout为零,则函数测试指定的时间对象的状态,并立即返回。如果dwTimeout是WSA_INFINITE,则函数的超时间隔永远不会到,最后一个参数fAlertable指定当系统针一个输入/输出完成例程放入队列以供执行时,函数是否返回,若为TRUE,则函数返回且执行完成例程,若为FALSE,函数 不返回,不执行完成例程,在win16中应忽略该参数。如果函数成功,返回值指出造成函数返回的事件对象,如果函数失败,返回值为WSA_WAIT_FAILED。
         这样,应用程序便可引用事件数组中已传信的事件,并检索与哪个事件对应的套接字,判断到底是在哪个套接字上,发生了什么网络事件类型。对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预声明值WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置)。如:
Index = WSAWaitForMultipleEvents(...);
MyEvent = EventArray[Index-WSA_WAIT_EVENT_0];
         知道了产生网络事件的套接字后,接下来可调用WSAEnumNetsorkEvents函数知道发生了什么类型的网络事件。该函数原型为:
int WSAEnumNetworkEvents(
  SOCKET s,
  WSAEVENT hEventObject,
  LPWSANETWORKEVENTS lpNetworkEvents
);
         其中,s参数标识套接字句柄,hEventObject对数是可选的,用于标识需要重设的相应事件对象,由于事件对象处在一个"已传信"状态,所以可将它传入,令其自动成为"未传信"状态。如果不想hEventObject参数来重设事件,那么可使用WSAResetEvent()函数;最后一个参数是lpNetworkEvents是一个WSANETWORKEVENTS结构的数组,每一个元素记录了一个网络事件和相应的错误代码。它WSANETWORKEVENTS结构格式为:
typedef struct _WSANETWORKEVENTS {
  long     lNetworkEvents;
  int      iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
其中,lNetworkEvents参数用于指定套接字上发生的所有网络事件类型。iErrorCode参数指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,只是要在事件名字后添加 一个"_BIT"后缀字串即可。如,对FD_READ事件类型来说,iErrorCode数组的索引标识符便是FD_READ_BIT。
完成了对WSANETWORKEVENTS结构中的事件处理后,应用程序可在所有可用的套接字上继续等待更多的网络事件.

4.重叠I/O模型
         winsock2引入了重叠I/O模型的概念并要求所有的传输协议提供者都支持这一功能。它的基本原理是让应用程序使用一个重叠的数据结构,一次投递一个或多个winsock I/O请求,针对那些提交的请求,在它们完成后,应用程序可为它们提供服务。应用程序可通过ReadFile和WriteFile两个函数执行I/O操作。重叠I/O仅能在由WSASocket函数(函数socket的增强版本)打开的套接实际上使用(使用 WSA_FLAG_OVERLAPPED标记)。这种方式的使用将采用win32建立的模型。
         对于接收,应用程序使用WSARecv函数或WSARecvFrom函数来提供存放接收数据的缓冲区。如果数据在网络接收前,应用程序已经提供了一个或多个数据缓冲区,那么接收的数据就可以立即存放进用户缓冲区。这样可省去使用recv函数和recvfrom函数时需要进行的拷贝工作。如果在应用程序提供数据区时,已经有数据到来,那么接收的数据将被立即拷贝进用户缓冲区。如果数据到来时,应用程序没有提供接收缓冲区,那么网络将回到我们熟悉的同步操作方式--传送来的数据将被存放进内部缓冲区,直到应用程序发出了接收调用并且提供了缓冲区,这时接收的数据就被拷贝进接收缓冲区。这种做法会有一个例外:就是当应用程序使用setsockopt函数把接收缓冲区长度轩为了0.在这种情况下,对于可靠舆协议,只有在应用程序提供了接收数据缓冲区后,数据才会被接收;而对于不可靠传输协议,灵气将会丢失。
         对于发送的一方,应用程序使用WSASend()函数或WSASendTo函数提供一个指向已填充的灵气缓冲区的指针。应用程序不应在网络使用完该缓冲区的数据以前以任何方式破坏该缓冲区的数据。
         重叠发送和接收调用会立即返回。如果返回值是SOCKET_ERROR,并且错误代码是WSA_IO_PENDING,那么表明重叠操作已经被成功的初始化,今后发送缓冲区被用完或接收缓冲区被填满时,将会有完成指示。任何其他的错误代码表明了初始化没有成功,今后也不会有什么完成指示。发送操作和接收操作都可以被重叠使用。接收函数可被多次调用,发出接收缓冲区,准备接收到来的数据。发送函数也可以被多次调用,组成 一个发送缓冲区队列。要注意的是,应用程序可以通过按顺序提供发送缓冲区来确保一毓重叠发送操作的顺序,但是对应的完成指示有可能是按另外的顺序排列的。同样的,在接收数据的一方,缓冲区是按被提供的顺序填充的,但完成指示也可能按另外的顺序排列。
         WSAIoctl函数(ioctolsocket函数的增强版本)还可以使用重叠I/O操作的延迟完成特性。