Winsock的异步模式的I/O模型
闲的没事看了下Winsock的异步模式的I/O模型,写些体会和感悟,记录一下。 1.Winsock同步阻塞方式的问题1 C0 l/ W8 {2 k 在异步非阻塞模式下,像accept(WSAAccept),recv(recv,WSARecv,WSARecvFrom)等这样的winsock函数调用后马上返回,而不是等待可用的连接和数据。在阻塞模式下,server往往这样等待client的连接:9 s: L) R8 f$ \ y' ^: z4 `3 T$ ] while(TRUE)2 g! H/ u u% z* @3 h {. e( R, C% _: I+ u; x) O9 Y //wait for a connection) o! l8 p$ W8 E ClientSocket = accept(ListenSocket,NULL,NULL);! ^! ^ a- w* ^" Y! |$ ]( A if(ClientSocket == INVALID_SOCKET) { ERRORHANDLE9 [! Y( @1 I! f6 A. z } else DoSomething7 K. I. u4 T* f8 ?, v' W! } } " A' ?0 t1 {" w Q8 v( {0 k 上述代码简单易用,但是缺点在于如果没有client连接的话,accept一直不会返回,而且即使accept成功创建会话套接字,在阻塞方式下,C/S间传输数据依然要将recv,send这类函数放到一个循环中,反复等待数据的到来,这种轮询的方式效率很低。为此,Winsock提供了异步模式的5种I/O模型,这些模型会在有网络事件(如socket收到连接请求,读取收到的数据请求等等)时通过监视集合(select),事件对象(WSAEventSelect,重叠I/O),窗口消息(WSAAsyncSelect),回调函数(重叠I/O),完成端口的方式通知程序,告诉我们可以“干活了”,这样的话大大的提高了执行效率,程序只需枕戈待旦,兵来将挡水来土掩,通知我们来什么网络事件,就做相应的处理即可。: }$ U0 R9 K5 T ` - x7 Y. B2 V8 l! v 2.WSAEventSelect模型的使用 # C- s( \+ K' b* a8 ~4 m6 F WSAEventSelect模型其实很简单,就是将一个事件对象同一个socket绑定并设置要监视的网络事件,当这个socket有我们感兴趣的网络事件到达时,ws2_32.dll就将这个事件对象置为受信状态(signaled),在程序中等待这个事件对象受信后,根据网络事件类型做不同的处理。如果对线程同步机制有些了解的话,这个模型很容易理解,其实就是CreateEvent系列的winsock版。 8 B! F9 a1 {. \# V! S; Y 无代码无真相,具体API的参数含义可以参考MSDN,MSDN上对这个模型解释的非常详尽。 Q, Y7 t# o2 R // 使用WSAEventSelect的代码片段,百度贴吧字数限制,略去错误处理及界面操作0 Y, g% ?1 p. R1 R1 A6 ~4 Y // 为了能和多个客户端通信,使用两个数组分别记录所有通信的会话套接字0 W8 I' {, L/ ?8 _% v/ W0 q9 k // 以及和这些套接字绑定的事件对象8 ?' w5 h/ }+ ^) n7 e( Z, z' G- X // WSA_MAXIMUM_WAIT_EVENTS是系统内部定义的宏,值为64 , _5 {7 q6 c1 Q. T" c% w1 Z SOCKET g_sockArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAEVENT g_eventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 事件对象计数器/ z8 q" T5 g3 Q8 W/ C int nEventTotal = 0; // 创建监听套接字sListenSocket,并对其绑定端口和本机ip 代码省去7 l: | I- Q6 z" J ........ // 设置sListenSocket为监听状态! U8 |$ _( q# c h6 A8 Y0 ] listen(sListenSocket, 5);- b/ a0 H, P, \3 E // 创建事件对象,同CreateEvent一样,event创建后被置为非受信状态 WSAEVENT acceptEvent = WSACreateEvent(); // 将sListenSocket和acceptEvent关联起来 // 并注册程序感兴趣的网络事件FD_ACCEPT 和 FD_CLOSE // 这里由于是在等待客户端connect,所以FD_ACCEPT和FD_CLOSE是我们关心的' N; K# P, c: q9 x WSAEventSelect(sListenSocket, acceptEvent, FD_ACCEPT|FD_CLOSE); // 添加到数组中 ~$ l. W2 [. ]6 _- b X g_eventArray[nEventTotal] = acceptEvent;! D+ i+ {0 d- C! O5 i$ o, [* N g_sockArray[nEventTotal] = sListenSocket; nEventTotal++;, \$ Q4 s$ a5 ~$ r // 处理网络事件 while(TRUE) {( n2 G I& N* p // 由于第三个参数是 FALSE,所以 g_eventArray 数组中有一个元素受信 WSAWaitForMultipleEvents 就返回 q2 H1 c1 k. P1 u( ? // 注意 返回值 nIndex 减去 WSA_WAIT_EVENT_0 的值才是受信事件在数组中的索引。 V' H5 h% @ [ // 如果有多个事件同时受信,函数返回索引值最小的那个。 // 由于第四个参数指定 WSA_INFINITE ,所以没有对象受信时会无限等待。2 q# s: n8 Y, k* F2 c6 q3 ~' k int nIndex = WSAWaitForMultipleEvents(nEventTotal, g_eventArray, FALSE, WSA_INFINITE, FALSE);) x s }+ p9 ~, Z 4 ] ?: S4 @5 ?, o5 O; i; { // 取得受信事件在数组中的位置。6 P2 y5 H. o! T' e$ s; I0 j nIndex = nIndex - WSA_WAIT_EVENT_0; . v: U$ D- ]$ Z! y/ r$ ? // 判断受信事件 g_eventArray[nIndex] 所关联的套接字 g_sockArray[nIndex] 的网络事件类型% B4 Q% c( e: a* R6 D, V( S3 i // MSDN中说如果事件对象不是NULL, WSAEnumNetworkEvents 会帮咱重置该事件对象为非受信,方便等待新的网络事件" z! D" n# N8 i' q e // 也就是说这里的 g_eventArray[nIndex] 变为非受信了,所以程序中不用再调用 WSAResetEvent了3 G J# w( h! U // WSANETWORKEVENTS 这个结构中 记录了关于g_sockArray[nIndex] 的网络事件和错误码 WSANETWORKEVENTS event; WSAEnumNetworkEvents(g_sockArray[nIndex], g_eventArray[nIndex], event);$ n( w# K0 b, ?* [+ a // 这里处理 FD_ACCEPT 这个网络事件6 s6 {5 r1 }+ G! ?1 V // event.lNetWorkEvents中记录的是网络事件类型( A4 o/ o F. J* {. j3 G$ g if(event.lNetworkEvents FD_ACCEPT) {0 l2 Q- Y, T; S* X+ `" f- b' | // event.iErrorCode是错误代码数组,event.iErrorCode[FD_ACCEPT_BIT] 为0表示正常 if(event.iErrorCode[FD_ACCEPT_BIT] == 0)- W) \ }) P& r' r { // 连接数超过系统约定的范围 if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS) { ErrorHandle...1 [$ \$ g# |/ r( @ c' { continue;8 G$ n9 U3 ` V% J Z0 k1 s5 E }8 `/ e3 P/ q5 V% t // 没有问题就可以accept了: p) W4 {1 L5 F( G( I# Z SOCKET sAcceptSocket = accept(g_sockArray[nIndex], NULL, NULL); H) U! ?% }) |; m // 新建的会话套接字用于C/S间的数据传输,所以这里关心FD_READ,FD_CLOSE,FD_WRITE三个事件* g9 {9 o" C2 X WSAEVENT event = WSACreateEvent(); WSAEventSelect(sAcceptSocket, event, FD_READ|FD_CLOSE|FD_WRITE); // 将新建的会话套接字及与该套接字关联的事件对象添加到数组中* S3 Y- n; p5 X/ \ l! s g_eventArray[nEventTotal] = event;1 L" L" p9 \1 M: K+ A2 a g_sockArray[nEventTotal] = sAcceptSocket; nEventTotal++; }. I k1 Q* M1 a0 y //event.iErrorCode[FD_ACCEPT_BIT] != 0 出错了 else, f6 d+ k* e- K! W6 c( w _ { ErrorHandle... e2 B% I r/ x# t! k. H break;" I/ p2 v' p# v, C; H } }/ r* L1 f8 I! {! n 6 v5 m0 g" d0 G# k ' q5 R X% \/ J // 这里处理FD_READ通知消息,当会话套接字上有数据到来时,ws2_32.dll会记录该事件 else if(event.lNetworkEvents FD_READ) {3 Q n7 y/ o3 V5 {' r& w$ w if(event.iErrorCode[FD_READ_BIT] == 0)$ ^ T5 e1 G0 Y2 @, k {) v8 K' G) v5 r: J ` int nRecv = recv(g_sockArray[nIndex], buffer, nbuffersize, 0); if(nRecv == SOCKET_ERROR) % n+ U) v) b3 T3 E { // 为了程序更鲁棒,这里要特别处理一下WSAEWOULDBLOCK这个错误 T- @- |4 V; E1 P( p) W) h // MSDN中说在异步模式下有时recv(WSARecv)读取时winsock的缓冲区中没有数据,导致recv立即返回" o4 v+ n) {# w+ N* C9 { h // 错误码就是 WSAEWOULDBLOCK,但这时程序并没有出问题,在有新的数据到来时recv还是可以读到数据的 // 所以不能仅仅根据recv返回值是SOCKET_ERROR就认为出错从而执行退出操作。3 D$ s) I+ u! V0 n3 A //如果错误码不是WSAEWOULDBLOCK 则表示真的出错了- w/ M) k2 T" z, E5 z/ l# F/ a6 x if(WSAGetLastError() != WSAEWOULDBLOCK) { $ {- D% M* u, y. Q+ q- {, v ErrorHandle... break;+ o X5 W: m: K$ i1 w }0 c2 t" l) p2 M: A K3 i' _ } // 没出任何错误* G# e! P* w. l+ S else DoSomeThing...$ \( A W( k0 b/ C# R0 U2 r3 c. o } f1 @+ D( ^0 L* i % z0 v3 y- E9 w/ u+ P- W // event.iErrorCode[FD_READ_BIT] != 0 else {/ ~5 k' x$ n7 b ErrorHandle... break; } } 5 _- r1 d" u" l0 o7 O0 b % L/ Z9 d T9 A0 F: t- d // 这里处理FD_CLOSE通知消息 // 当连接被关闭时,ws2_32.dll会记录FD_CLOSE事件1 ]. b' I, M" Y2 R5 W6 c4 }5 V else if(event.lNetworkEvents FD_CLOSE) { if(event.iErrorCode[FD_CLOSE_BIT] == 0)( b) r0 p" B% o7 |, x, C {+ d5 e' V+ ?' @" Q) c5 O closesocket(g_sockArray[nIndex]); // 将g_sockArray[nIndex]从g_sockArray数组中删除: [% D5 i. ?1 ] for(int j=nIndex; j<nEventTotal-1; j++)% |( S. s2 _$ y, C( _. h, j g_sockArray[j] = g_sockArray[j+1]; T8 M9 ^+ A b0 J7 b nEventTotal--;1 S6 W0 ?' K" ~- [ t" K' B% R } // event.iErrorCode[FD_CLOSE_BIT] != 0 else { ErrorHandle..." k- M" Z+ K1 l) c+ `/ N$ C break;, q# c6 ~' W; W, X: z4 A8 {: p6 J }/ F6 m) D- a5 y+ |8 v J } 0 ~: E- {0 d2 I8 C) n // 处理FD_WRITE通知消息 // FD_WRITE事件其实就是ws2_32.dll告诉我们winsock的缓冲区已经ok,可以发送数据了 // 同recv一样,send(WSASend)的返回值也要对SOCKET_ERROR特殊判断一下 WSAEWOULDBLOCK6 N. O% X7 t- ~ else if(event.lNetworkEvents FD_WRITE) 0 \5 h7 {( D# {* E, B% k$ ^' W {% J, p J1 N5 I+ Q G# ^8 J; K; y //关于FD_WRITE的讨论在下面。; k5 O' I/ ~( o7 V' K }( \1 [3 p8 b1 b x4 V ^7 F+ f }! Q$ B4 O9 s9 ~5 s8 P * s/ g) X$ Q6 q7 ]- S4 } // 如果出错退出循环 则将套接字数组中的套接字与事件对象统统解除关联) w0 [3 @7 R5 Y" }2 R // 给WSAEventSelect的最后一个参数传0可以解除g_sockArray[nIndex]和g_eventArray[nIndex]的关联# l4 b; f! [$ c0 F8 k // 解除关联后,ws2_32.dll将停止记录g_sockArray[nIndex]这个套接字的网络事件 // 退出时还要关闭所有创建的套接字和事件对象! x3 K" k u" r2 s for(int i = 0; i < nEventTotal; i++) {9 _8 M: K; i- J, Q w WSAEventSelect(g_sockArray[i], g_eventArray[i], 0); closesocket(g_sockArray[i]); WSACloseEvent(g_eventArray[i]);/ v9 I( ~0 F, C1 r0 X: x0 i } ; A( c8 t7 z- d: ^9 ~ nEventTotal = 0; " S: w. _* S' j/ Q9 B DoSomethingElse....1 `# O% L( c5 G 0 J( [# r+ @/ M) v' r5 i2 P9 z1 g; A + x6 L' m B5 ?; b* | 3.FD_WRITE 事件的触发$ x8 l/ B- N! i- P$ N8 s) ~3 \8 Q 常见的网络事件中,FD_ACCEPT和FD_READ都比较好理解。一开始我唯一困惑的就是FD_WRITE,搞不清楚到底什么时候才会触发这个网络事件,后来仔细查了MSDN又看了一些文章并测试了下,终于搞懂了FD_WRITE的触发机制。 下面是MSDN中对FD_WRITE触发机制的解释: The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure the application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated eventobject is set6 n4 T+ v B. r' G* B * \9 M" ^' Y7 A t" q# n0 t FD_WRITE事件只有在以下三种情况下才会触发; l5 h3 z; ^0 C j( {7 ^ 4 G; N4 s6 T# M( g0 D) t3 [( M3 ?6 f ①client 通过connect(WSAConnect)首次和server建立连接时,在client端会触发FD_WRITE事件 ②server通过accept(WSAAccept)接受client连接请求时,在server端会触发FD_WRITE事件 ③send(WSASend)/sendto(WSASendTo)发送失败返回WSAEWOULDBLOCK,并且当缓冲区有可用空间时,则会触发FD_WRITE事件 ①②其实是同一种情况,在第一次建立连接时,C/S端都会触发一个FD_WRITE事件。. Z) ] N- l7 X K; S) [' H, J " q: \, G5 B2 Q4 l0 ` 主要是③这种情况:send出去的数据其实都先存在winsock的发送缓冲区中,然后才发送出去,如果缓冲区满了,那么再调用send(WSASend,sendto,WSASendTo)的话,就会返回一个 WSAEWOULDBLOCK的错误码,接下来随着发送缓冲区中的数据被发送出去,缓冲区中出现可用空间时,一个 FD_WRITE 事件才会被触发,这里比较容易混淆的是 FD_WRITE 触发的前提是 缓冲区要先被充满然后随着数据的发送又出现可用空间,而不是缓冲区中有可用空间,也就是说像如下的调用方式可能出现问题' O2 \# M u5 W& J0 e : B) k" t- C# c5 e4 ~ else if(event.lNetworkEvents FD_WRITE) {6 A1 N- B7 h5 |5 Y1 @% K0 C1 V. |9 R" S if(event.iErrorCode[FD_WRITE_BIT] == 0)3 {, S4 _+ {# E- P) U { send(g_sockArray[nIndex], buffer, buffersize);! u5 `3 ]" e2 C9 V' b3 q* ~ .... }' H1 [ D. i ^- W( G* G else2 a8 x' }/ Y N3 W/ G {% o' X7 _5 m; J* A* L } } 问题在于建立连接后 FD_WRITE 第一次被触发, 如果send发送的数据不足以充满缓冲区,虽然缓冲区中仍有空闲空间,但是 FD_WRITE 不会再被触发,程序永远也等不到可以发送的网络事件。" ?' l4 x1 p7 i7 @ 基于以上原因,在收到FD_WRITE事件时,程序就用循环或线程不停的send数据,直至send返回WSAEWOULDBLOCK,表明缓冲区已满,再退出循环或线程。当缓冲区中又有新的空闲空间时,FD_WRITE 事件又被触发,程序被通知后又可发送数据了。8 w1 ^ j' [5 c% e- O) m& ` ! b7 I4 Y# W* D/ P1 Z" e 上面代码片段中省略的对 FD_WRITE 事件处理. K0 H7 G% h1 L% q g1 `5 ?# S else if(event.lNetworkEvents FD_WRITE)0 J$ ^# }- O p% b7 f: f { if(event.iErrorCode[FD_WRITE_BIT] == 0). @1 z0 R3 l3 b {3 V3 \' {! a9 x' H o while(TRUE); o; Q% s8 ]. M2 L7 f! x4 o {$ p% F- |; L Z0 z a0 ]0 g! P // 得到要发送的buffer,可以是用户的输入,从文件中读取等1 S; }9 N3 G6 B9 m! i- ^* N+ |9 M7 z GetBuffer.... q7 ?: x: D: I, T if(send(g_sockArray[nIndex], buffer, buffersize, 0) == SOCKET_ERROR) { // 发送缓冲区已满9 d# R. h+ U* ^+ Q if(WSAGetLastError() == WSAEWOULDBLOCK) break;% V" w d5 l0 @, w else ErrorHandle...; j* y5 B" W" v9 R" k } } }# b6 m" u5 T# ?: z; z" K# g else { ErrorHandle..: N6 D% e- [5 _2 z# W# C3 L break;6 ~3 S4 ~' v) N' I# }" \ } } P.S. : L1 T, ]$ F7 t+ ]0 W 1.WSAWaitForMultipleEvents内部调用的还是WaitForMulipleObjectsEx,MSDN中说使用WSAEventSelect模型等待时是不占cpu时间的,这也是效率比阻塞winsock高的原因。3 _( j- P1 T4 l$ r/ Z2 S * W# n. j H2 s; U. J0 J; E 2.WSAAsycSelect的用法和WSAEventSelect类似,不同的是网络事件的通知是以windows消息的方式发送到指定的窗口。 |