网络编程事件模型和窗口消息模型中FD_WRITE的理解要点

网络工程相关 专栏收录该内容
20 篇文章 0 订阅

1.Winsock同步阻塞方式的问题

在异步非阻塞模式下,像accept(WSAAccept),recv(recv,WSARecv,WSARecvFrom)等这样的winsock函数调用后马上返回,而不是等待可用的连接和数据。在阻塞模式下,server往往这样等待client的连接:

 

while(TRUE)
{
    //wait for a connection
     ClientSocket = accept(ListenSocket,NULL,NULL);
    if(ClientSocket == INVALID_SOCKET)
     {
         ERRORHANDLE
     }
     else
         DoSomething
}

 

 

 

上述代码简单易用,但是缺点在于如果没有client连接的话,accept一直不会返回,而且即使accept成功创建会话套接字,在阻塞方式下,C/S间传输数据依然要将recv,send这类函数放到一个循环中,反复等待数据的到来,这种轮询的方式效率很低。为此,Winsock提供了异步模式5种I/O模型,这些模型会在有网络事件(如socket收到连接请求,读取收到的数据请求等等)时通过监视集合(select)事件对象(WSAEventSelect,重叠I/O),窗口消息(WSAAsyncSelect)回调函数(重叠I/O),完成端口的方式通知程序,告诉我们可以“干活了”,这样的话大大的提高了执行效率,程序只需枕戈待旦,兵来将挡水来土掩,通知我们来什么网络事件,就做相应的处理即可。

2.WSAEventSelect模型的使用

WSAEventSelect模型其实很简单,就是将一个事件对象同一个socket绑定并设置要监视的网络事件,当这个socket有我们感兴趣的网络事件到达时,ws2_32.dll就将这个事件对象置为受信状态(signaled),在程序中等待这个事件对象受信后,根据网络事件类型做不同的处理。如果对线程同步机制有些了解的话,这个模型很容易理解,其实就是CreateEvent系列的winsock版。

无代码无真相,具体API的参数含义可以参考MSDN,MSDN上对这个模型解释的非常详尽。

// 使用WSAEventSelect的代码片段,百度贴吧字数限制,略去错误处理及界面操作
    // 为了能和多个客户端通信,使用两个数组分别记录所有通信的会话套接字
    // 以及和这些套接字绑定的事件对象
    // WSA_MAXIMUM_WAIT_EVENTS是系统内部定义的宏,值为64

     SOCKET g_sockArray[WSA_MAXIMUM_WAIT_EVENTS];
     WSAEVENT g_eventArray[WSA_MAXIMUM_WAIT_EVENTS];

    // 事件对象计数器
    int nEventTotal = 0;

    // 创建监听套接字sListenSocket,并对其绑定端口和本机ip 代码省去
     ........

    // 设置sListenSocket为监听状态
     listen(sListenSocket, 5);

    // 创建事件对象,同CreateEvent一样,event创建后被置为非受信状态
     WSAEVENT acceptEvent = WSACreateEvent();

    // 将sListenSocket和acceptEvent关联起来
    // 并注册程序感兴趣的网络事件FD_ACCEPT 和 FD_CLOSE
    // 这里由于是在等待客户端connect,所以FD_ACCEPT和FD_CLOSE是我们关心的
     WSAEventSelect(sListenSocket, acceptEvent, FD_ACCEPT|FD_CLOSE);

    // 添加到数组中
     g_eventArray[nEventTotal] = acceptEvent;
     g_sockArray[nEventTotal] = sListenSocket;    
     nEventTotal++;

    // 处理网络事件
    while(TRUE)
     {
        // 由于第三个参数是 FALSE,所以 g_eventArray 数组中有一个元素受信 WSAWaitForMultipleEvents 就返回
        // 注意 返回值 nIndex 减去 WSA_WAIT_EVENT_0 的值才是受信事件在数组中的索引。
        // 如果有多个事件同时受信,函数返回索引值最小的那个。
        // 由于第四个参数指定 WSA_INFINITE ,所以没有对象受信时会无限等待。
        int nIndex = WSAWaitForMultipleEvents(nEventTotal, g_eventArray, FALSE, WSA_INFINITE, FALSE);

        // 取得受信事件在数组中的位置。
         nIndex = nIndex - WSA_WAIT_EVENT_0;

        // 判断受信事件 g_eventArray[nIndex] 所关联的套接字 g_sockArray[nIndex] 的网络事件类型
        // MSDN中说如果事件对象不是NULL, WSAEnumNetworkEvents 会帮咱重置该事件对象为非受信,方便等待新的网络事件
        // 也就是说这里的 g_eventArray[nIndex] 变为非受信了,所以程序中不用再调用 WSAResetEvent了
        // WSANETWORKEVENTS 这个结构中 记录了关于g_sockArray[nIndex] 的网络事件和错误码
         WSANETWORKEVENTS event;
         WSAEnumNetworkEvents(g_sockArray[nIndex], g_eventArray[nIndex], &event);

        // 这里处理 FD_ACCEPT 这个网络事件
        // event.lNetWorkEvents中记录的是网络事件类型
        if(event.lNetworkEvents & FD_ACCEPT)
         {
            // event.iErrorCode是错误代码数组,event.iErrorCode[FD_ACCEPT_BIT] 为0表示正常
            if(event.iErrorCode[FD_ACCEPT_BIT] == 0)
             {
                // 连接数超过系统约定的范围
                if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
                 {    
                     ErrorHandle...
                    continue;
                 }
                // 没有问题就可以accept了
                 SOCKET sAcceptSocket = accept(g_sockArray[nIndex], NULL, NULL);

                // 新建的会话套接字用于C/S间的数据传输,所以这里关心FD_READ,FD_CLOSE,FD_WRITE三个事件
                 WSAEVENT event = WSACreateEvent();
                 WSAEventSelect(sAcceptSocket, event, FD_READ|FD_CLOSE|FD_WRITE);

                // 将新建的会话套接字及与该套接字关联的事件对象添加到数组中
                 g_eventArray[nEventTotal] = event;
                 g_sockArray[nEventTotal] = sAcceptSocket;    
                 nEventTotal++;
             }

            //event.iErrorCode[FD_ACCEPT_BIT] != 0 出错了
             else
             {
                 ErrorHandle...
                break;
             }
         }


        // 这里处理FD_READ通知消息,当会话套接字上有数据到来时,ws2_32.dll会记录该事件
         else if(event.lNetworkEvents & FD_READ)    
         {
            if(event.iErrorCode[FD_READ_BIT] == 0)
             {
                int nRecv = recv(g_sockArray[nIndex], buffer, nbuffersize, 0);
                if(nRecv == SOCKET_ERROR)                
                 {
                    // 为了程序更鲁棒,这里要特别处理一下WSAEWOULDBLOCK这个错误
                    // MSDN中说在异步模式下有时recv(WSARecv)读取时winsock的缓冲区中没有数据,导致recv立即返回
                    // 错误码就是 WSAEWOULDBLOCK,但这时程序并没有出问题,在有新的数据到来时recv还是可以读到数据的
                    // 所以不能仅仅根据recv返回值是SOCKET_ERROR就认为出错从而执行退出操作。
                    //如果错误码不是WSAEWOULDBLOCK 则表示真的出错了
                    if(WSAGetLastError() != WSAEWOULDBLOCK)
                     {    
                         ErrorHandle...
                        break;
                     }
                 }
                // 没出任何错误
                 else
                     DoSomeThing...
             }

            // event.iErrorCode[FD_READ_BIT] != 0
             else
             {
                 ErrorHandle...
                break;
             }
         }


        // 这里处理FD_CLOSE通知消息
        // 当连接被关闭时,ws2_32.dll会记录FD_CLOSE事件
         else if(event.lNetworkEvents & FD_CLOSE)
         {
            if(event.iErrorCode[FD_CLOSE_BIT] == 0)
             {
                 closesocket(g_sockArray[nIndex]);
                                 // 将g_sockArray[nIndex]从g_sockArray数组中删除
                for(int j=nIndex; j<nEventTotal-1; j++)
                     g_sockArray[j] = g_sockArray[j+1];    
                 nEventTotal--;
             }

            // event.iErrorCode[FD_CLOSE_BIT] != 0
             else
             {
                 ErrorHandle...
                break;
             }
         }


        // 处理FD_WRITE通知消息
        // FD_WRITE事件其实就是ws2_32.dll告诉我们winsock的缓冲区已经ok,可以发送数据了
        // 同recv一样,send(WSASend)的返回值也要对SOCKET_ERROR特殊判断一下 WSAEWOULDBLOCK
         else if(event.lNetworkEvents & FD_WRITE)        
         {
            //关于FD_WRITE的讨论在下面。
         }
     }

    // 如果出错退出循环 则将套接字数组中的套接字与事件对象统统解除关联
    // 给WSAEventSelect的最后一个参数传0可以解除g_sockArray[nIndex]和g_eventArray[nIndex]的关联
    // 解除关联后,ws2_32.dll将停止记录g_sockArray[nIndex]这个套接字的网络事件
    // 退出时还要关闭所有创建的套接字和事件对象

    for(int i = 0; i < nEventTotal; i++)
     {
         WSAEventSelect(g_sockArray[i], g_eventArray[i], 0);    
         closesocket(g_sockArray[i]);
         WSACloseEvent(g_eventArray[i]);
     }

     nEventTotal = 0;

     DoSomethingElse....

 

3.FD_WRITE 事件的触发

常见的网络事件中,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 event object is set

FD_WRITE事件只有在以下三种情况下才会触发

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事件。

主要是③这种情况:send出去的数据其实都先存在winsock的发送缓冲区中,然后才发送出去如果缓冲区满了,那么再调用send(WSASend,sendto,WSASendTo)的话,就会返回一个 WSAEWOULDBLOCK的错误码,接下来随着发送缓冲区中的数据被发送出去,缓冲区中出现可用空间时,一个 FD_WRITE 事件才会被触发,这里比较容易混淆的是 FD_WRITE 触发的前提是 缓冲区要先被充满然后随着数据的发送又出现可用空间而不是缓冲区中有可用空间,也就是说像如下的调用方式可能出现问题

 

else if(event.lNetworkEvents & FD_WRITE)
{
    if(event.iErrorCode[FD_WRITE_BIT] == 0)
     {
         send(g_sockArray[nIndex], buffer, buffersize);
         ....
     }
     else
     {
     }
}

 

 

 

问题在于建立连接后 FD_WRITE 第一次被触发, 如果send发送的数据不足以充满缓冲区,虽然缓冲区中仍有空闲空间,但是 FD_WRITE 不会再被触发,程序永远也等不到可以发送的网络事件。

基于以上原因,在收到FD_WRITE事件时,程序就用循环或线程不停的send数据,直至send返回WSAEWOULDBLOCK,表明缓冲区已满,再退出循环或线程。当缓冲区中又有新的空闲空间时,FD_WRITE 事件又被触发,程序被通知后又可发送数据了。

上面代码片段中省略的对 FD_WRITE 事件处理

 

else if(event.lNetworkEvents & FD_WRITE)
{
    if(event.iErrorCode[FD_WRITE_BIT] == 0)
     {
        while(TRUE)
         {
            // 得到要发送的buffer,可以是用户的输入,从文件中读取等
             GetBuffer....
            if(send(g_sockArray[nIndex], buffer, buffersize, 0) == SOCKET_ERROR)
             {
                // 发送缓冲区已满
                if(WSAGetLastError() == WSAEWOULDBLOCK)
                    break;
                 else
                     ErrorHandle...
             }
         }
     }
     else
     {
         ErrorHandle..
        break;
     }
}

 

补充:

1.WSAWaitForMultipleEvents内部调用的还是WaitForMulipleObjectsEx,MSDN中说使用WSAEventSelect模型等待时是不占cpu时间的,这也是效率比阻塞winsock高的原因。

2.WSAAsycSelect的用法和WSAEventSelect类似,不同的是网络事件的通知是以windows消息的方式发送到指定的窗口。

 

来自:http://oliver258.blog.51cto.com/750330/423813

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值