c++Socket 异步通讯

时间:2023-02-09 07:17:11
在网络通讯中,由于网络拥挤或一次发送的数据量过大等原因,经常会发生交换的数据在短时间内不能传送完,收发数据的函数因此不能返回,这种现象叫做阻塞。 Winsock对有可能阻塞的函数提供了两种处理方式:阻塞和非阻塞方式。

阻塞模式

在阻塞方式下,收发数据的函数在被调用后一直要到传送完毕或者出错才能返回。在阻塞期间,被阻的函数不会断调用系统函数GetMessage()来保持消息循环的正常进行。

非阻塞模式
        将一个套接字置为非阻塞模式之后, Winsock API调用会立即返回。一般这些调用都会“失败”,并返回一个WSAEWOULDBLOCK。表明其操作在调用期间没有时间完成。如在系统的输入缓冲区中,并不存在等待的数据,那recv调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。这不是一个好的方法。通常采用Winsock的套接字I/O模型去处理。

套接字I/O模型共有五种类型,如下:

select(选择) 
  WSAAsyncSelect(异步选择)
  WSAEventSelect(事件选择)
  overlapped(重叠)
  completion port(完成端口)

*WSAAsyncSelect

Winsock通过WSAAsyncSelect()自动地设置套接字处于非阻塞方式。使用WindowsSockets实现Windows网络程序设计的关键就是它提供了对网络事件基于消息的异步存取,用于注册应用程序感兴趣的网络事件。它请求Windows Sockets DLL在检测到套接字上发生的网络事件时,向窗口发送一个消息。

int PASCAL FAR WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent);
hWnd:窗口句柄
wMsg:需要发送的消息
lEvent:事件(以下为事件的内容)
值: 含义:
FD_READ 期望在套接字上收到数据(即读准备好)时接到通知
FD_WRITE 期望在套接字上可发送数据(即写准备好)时接到通知
FD_OOB 期望在套接字上有带外数据到达时接到通知
FD_ACCEPT 期望在套接字上有外来连接时接到通知
FD_CONNECT 期望在套接字连接建立完成时接到通知
FD_CLOSE 期望在套接字关闭时接到通知

进行异步选择使用WSAAsyncSelect()函数时,有以下几点需要引起特别的注意:
  .连续使用两次WSAAsyncSelect()函数时,只有第二次设置的事件有效,如:
           WSAAsyncSelect(s,hwnd,wMsg1,FD_READ);
           WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE);
        这样只有当FD_CLOSE事件发生时才会发送wMsg2消息。
  .可以在设置过异步选择后通过再次调用WSAAsyncSelect(s,hwnd,0,0);的形式取消在套接字上所设置的异步事件。
  .Windows Sockets DLL在一个网络事件发生后,通常只会给相应的应用程序发送一个消息,而不能发送多个消息。但通过使用一些函数隐式地允许重发此事件的消息,这样就可能再次接收到相应的消息。
  .在调用过closesocket()函数关闭套接字之后不会再发生FD_CLOSE事件。

对UDP协议,这些网络事件主要为:
      FD_READ   期望在套接字收到数据(即读准备好)时接收通知;
      FD_WRITE 期望在套接字可发送数(即写准备好)时接收通知;
    FD_CLOSE 期望在套接字关闭时接电通知
  消息变量wParam指示发生网络事件的套接字,变量1Param的低字节描述发生的网络事件,高字包含错误码。如在窗口函数的消息循环中均加一个分支:
int ok=sizeof(SOCKADDR);
case wMsg;
switch(1Param)
{
    case FD_READ:  //套接字上读数据 
    if(recvfrom(sr.lpPlayData[j],dwDataSize,0,(struct sockaddr FAR*)&there1,
     (int FAR*)&ok)==SOCKET_ERROR0) {
                MessageBox(hwnd,“数据接收失败!”,“”,MB_OK);
                return(FALSE);
       }
    case FD_WRITE:    //套接字上写数据
  }
break;

*WSAEventSelect
      事件通知模型要求在程序中针对使用的每个套接字创建一个事件对象,然后通过事件模式通知程序其套接字是否收到或发送的信息。一般来说这种模式,一般就是通过类似调用waitformultipleObject一样在一个线程中等待信号事件来,来了就处理。具体调用的函数如下:

创建WSACreateEvent函数.该函数的返回值是一个创建好的事件对象句柄。事件对象句柄完后,接下来将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型,方法是调用WSAEventSelect函数,对它的定义如下:

int WSAEventSelect (
              SOCKET s,                              //需要非阻塞处理的套接字
              WSAEVENT hEventObject,    //WSACreateEvent 创建来的,关联到socket
              long lNetworkEvents     
               );
    lNetworkEvents,对应一个“位掩码”,用于指定应用程序感兴趣的各种网络事件类型的一个组合。要想获知对这些事件类型的详细说明,请参考早先讨论过的WSAAsyncSelect I/O模型。
      为WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。
    两种工作状态分别是“已传信”(signaled)和 “未传信”(nonsignaled)。
    工作模式则包括“人工”(manual reset)和“自动”(auto reset)。

WSACreateEvent缺省时其信号状态为0,且为人工设置,当网络事件触发了与一个套接字关联在一起的事件对象,其事件信号置1。在完成了一个I/O请求的处理之后,需要调用WSAResetEvent复位处理(置信号为0)。
     一个套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。
     一般而言,在等待网络传来事件时,类似WaitforMultipleObject,其WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入有信号状态后,或在超过了一个规定的时间周期后,立即返回(线程往往在这里死等)。

下面是 WSAWaitForMultipleEvents函数的定义:
DWORD WSAWaitForMultipleEvents(
  DWORD cEvents,                 
  const WSAEVENT FAR *lphEvents
  BOOL fWaitAll,                 
  DWORD dwTimeOUT,               
  BOOL fAlertable                
);

其用法和WaitForMultipleObject类似。
cEvents和lphEvents参数定义了由WSAEVENT对象构成的一个数组。在这个数组中,cEvents指定的是事件对象的数量,而lphEvents对应的是一个指针,用于直接引用该数组。
     要注意的是, WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS对象规定的一个最大值,在此定义成64个。故该I/O模型一次最多都只能支持64个套接字。假如想让这个模型同时管理不止64个套接字,必须创建更多的工作者线程,以便等待更多的事件对象。

fWaitAl l 参数指定了WSAWaitForMultiple Events如何等待在事件数组中的对象。
   =TRUE,那么只有等lphEvents数组内包含的所有事件对象都处于有信号状态,函数才会返回;

=FALSE,任一个事件对象进入有信号时,函数就会返回。

dwTimeout参数规定了 WSAWaitForMultipleEvents最多可等待一个网络事件发生有多长时间。超过规定的时间,函数就会立即返回。并返回WSA_WAIT_TIMEOUT。如dwsTimeout设为WSA_INFIN ITE(永远等待),那么根据fWaiiAll或等待一个网络事件或所有网络事件都传信号后,才能从该函数退出。
 fAlertable,缺省设为FALSE。主要用于在重叠式I/O模型中.

当设置fWaiAll=false,WaitForMultipleObject再有网络事件时,会返回一个值,指出造成函数返回的事件对象。根据WSAWaitForMultipleEvents的返回值,减去预定义值WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置),程序便可用事件数组中已发信号的事件,检索与那个事件对应的套接字,知道了造成网络事件的套接字后,调用 WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件。该函数定义如下:
int WSAEnumNetworkEvents (
  SOCKET s,                                      //检索该套接字
  WSAEVENT hEventObject,             
  LPWSANETWORKEVENTS lpNetworkEvents 
);
    hEventObject参数则是可选的;它指定了一个事件句柄,对应于打算重设的那个事件对象。由于我们的事件对象处在一个有信号状态,所以可将它传入,令其自动成为无信号状态。
    也可以采用使用 WSAResetEvent 函数复位事件信号。
   lpNetworkEvents,就是返回的结果信息,它是一个指向WSANETWORKEVENTS结构的指针,用于接收套接字上发生的网络事件类型以及可能出现的任何错误代码。

其WSANETWORKEVENTS结构的定义:
typedef struct _WSANETWORKEVENTS
{
     long lNetworkEvents;
     int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
lNetworkEvents参数指定了一个值,对应于套接字上发生的所有网络事件类型。
      注意一个事件进入置1(有信号)状态时,可能会同时发生多个网络事件类型。如,一个忙的服务器可能同时收到FD_READ和FD_WRITE通知。 iErrorCode参数指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,只是要在事件名字后面添加一个“ _BIT”后缀字串即可。如,对FD_READ事件类型来说,iErrorCode数组的索引标识符便是FD_READ_BIT。