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


《Windows Sockets网络编程》是WindowsSockets网络编程领域公认的经典著作,由Windows Sockets2.0规范解释小组负责人亲自执笔,权威性毋庸置疑。它结合大量示例,对WindowsSockets规范进行了深刻地解读,系统讲解了WindowsSockets网络编程及其相关的概念、原理、主要命令、操作模式,以及开发技巧和可能的陷阱,从程序员的角度给出了大量的建议和最佳实践,是学习WindowsSockets网络编程不可多得的参考书。   全书分为三部分:第一部分(第1~6章),提供了翔实的背景知识和框架方面的概念,借助于此框架,读者可理解WinSock的具体细节,包括WindowsSockets概述、OSI网络参考模型、TCP/IP协议簇中的协议和可用的服务、WinSock网络应用程序的框架及其工作机制、WinSock的三种操作模式socket通信机制等;第二部分(第7~12章),以FTP客户端实例为基础介绍了函数实例库,还介绍了客户端程序、服务器程序和DLL中间构件及它们的相应函数,并涵盖socket命令和选项及移植BSDSockets相关事项等;第三部分(第13~17章),介绍了应用程序调试技术和工具,针对应用编程中的陷阱的建议和措施,WinSockAPI的多种操作系统平台,WinSock规范的可选功能和WinSock规范2.0中的所有新功能。 译者序 序 前言 第1章 Windows Sockets概述 1.1 什么是Windows Sockets 1.2 Windows Sockets的发展历史 1.3 Windows Sockets的优势 1.3.1 Windows Sockets是一个开放的标准 1.3.2 Windows Sockets提供源代码可移植性 1.3.3 Windows Sockets支持动态链接 1.3.4 Windows Sockets的优点 1.4 Windows Sockets的前景 1.5 结论 第2章 Windows Sockets的概念 2.1 OSI网络模型 2.2 WinSock网络模型 2.2.1 信息与数据 2.2.2 应用协议 2.3 WinSock中的OSI层次 2.3.1 应用层 2.3.2 表示层 2.3.3 会话层 2.3.4 传输层 2.3.5 网络层 2.3.6 数据链路层 2.3.7 物理层 2.4 模块化的层次框 2.5 服务和协议 2.6 协议和API 第3章 TCP/IP协议服务 3.1 什么是TCP/IP 3.2 TCP/IP的发展历史 3.3 传输服务 3.3.1 无连接的服务:UDP 3.3.2 面向连接的服务:TCP 3.3.3 传输协议的选择:UDP与TCP的对比 3.4 网络服务 3.4.1 IP服务 3.4.2 ICMP服务 3.5 支持协议和服务 3.5.1 域名服务 3.5.2 地址解析协议 3.5.3 其他支持协议 3.6 TCP/IP的发展前景 第4章 网络应用程序工作机制 4.1 客户端-服务器模型 4.2 网络程序概览 4.3 socket的打开 4.4 socket的命名 4.4.1 sockaddr结构 4.4.2 sockaddr_in结构 4.4.3 端口号 4.4.4 本地IP地址 4.4.5 什么是socket名称 4.4.6 客户端socket名称是可选的 4.5 与另一个socket建立关联 4.5.1 服务器如何准备建立关联 4.5.2 客户端如何发起一个关联 4.5.3 服务器如何完成一个关联 4.6 socket之间的发送与接收 4.6.1 在“已连接的”socket上发送数据 4.6.2 在“无连接的”socket上发送数据 4.6.3 接收数据 4.6.4 socket解复用器中的关联 4.7 socket的关闭 4.7.1 closesocket 4.7.2 shutdown 4.8 客户端和服务器概览 第5章 操作模式 5.1 什么是操作模式 5.1.1 不挂机,等待:阻塞 5.1.2 挂机后再拨:非阻塞 5.1.3 请求对方回拨:异步 5.2 阻塞模式 5.2.1 阻塞socket 5.2.2 阻塞函数 5.2.3 伪阻塞的问题 5.2.4 阻塞钩子函数 5.2.5 阻塞情境 5.2.6 撤销阻塞操作 5.2.7 阻塞操作中的超时 5.2.8 无最少接收限制值 5.2.9 代码示例 5.3 非阻塞模式 5.3.1 怎样使socket成为非阻塞的 5.3.2 成功与失败不是绝对的 5.3.3 探询而非阻塞 5.3.4 显式地避让 5.3.5 代码示例 5.4 异步模式 5.4.1 认识异步函数 5.4.2 撤销异步操作 5.4.3 代码示例 5.4.4 AU_T
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值