现在的位置: 首页 > 综合 > 正文

Winsock的异步模式的I/O模型

2014年02月20日 ⁄ 综合 ⁄ 共 9937字 ⁄ 字号 评论关闭

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消息的方式发送到指定的窗口。

【上篇】
【下篇】

抱歉!评论已关闭.