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

41 篇文章 0 订阅
12 篇文章 0 订阅

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值