i/o模型——浅理解

 老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
这和Socket模型非常类似。下面我就以老陈接收信件为例讲解Socket I/O模型~~~

一:select模型

老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信~~~~~
在这种情况下,"下楼检查信箱"然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收/ 发送.......

使用线程来select应该是通用的做法:


二:WSAAsyncSelect模型

后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天......不是,微软~~~~~~~~
微软提供的WSAAsyncSelect模型就是这个意思。

WSAAsyncSelect 模型是Windows下最简单易用的一种Socket I/O模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
 
三:WSAEventSelect模型

后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝啥都不好使~~~~~~
微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出"新信件到达"声,提醒老陈去收信。盖茨终于可以睡觉了。

四:Overlapped I/O 事件通知模型

后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!

Overlapped I/O 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在"Overlapped",Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区~~~~~

五:Overlapped I/O 完成例程模型

老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件......为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读 /回复了!老陈终于过上了小资生活!

六:IOCP模型

微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏......
微软给每个大公司派了一名名叫"Completion Port"的超级机器人,让这个机器人去处理那些信件!

"Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多 CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?"



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操作的延迟完成特性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值