windows 下 WSAAsyncSelect模型

来源:互联网 发布:读书无用论 知乎 编辑:程序博客网 时间:2024/06/15 17:56

      WSAAsyncSelec是Winsock提供的一个适合于Windows编程使用的函数,它允许在一个套接口上当发生特定的网络事件时,给Windows网络应用程序(窗口或对话框)发送一个消息(事件通知)。


  WSAAsyncSelect模型select模型的相同点是它们都可以对多个套接字进行管理。但它们也有不小的区别。首先WSAAsyncSelect模型是异步的,且通知方式不同。更重要的一点是:WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口,而select模型可以广泛应用在Unix系统,使用该模型不需要创建窗口。最后一点区别:应用程序在调用WSAAsyncSelect函数后,套接字就被设置为非阻塞状态。而使用select函数不改变套接字的工作方式。



WSAAsyncSelect函数原型如下:

int WSAAsyncSelect(    SOCKET s,  //标识一个需要事件通知的套接口描述符    HWND hWnd,  //标识一个在网络事件发生时要想收到消息的窗口或对话框的句柄    u_int wMsg,  //在网络事件发生时要接收的消息,该消息会投递到由hWnd句柄指定的窗口或对话框    long lEvent  //位屏蔽码,用于指明应用程序感兴趣的网络事件集合);


常用的网络事件如下:

     FD_READ:套接字可读通知。

     FD_WRITE:可写通知。

     FD_ACCEPT:服务器接收连接的通知。

     FD_CONNECT:有客户连接通知。

     FD_OOB:外带数据到达通知。

     FD_CLOSE:套接字关闭通知。

     FD_QOS:服务质量发生变化通知。

     FD_GROUP_QOS:组服务质量发生变化通知。

     FD_ROUTING_INTERFACE_CHANGE:与路由器接口发生变化的通知。

     FD_ADDRESS_LIST_CHANGE:本地地址列表发生变化的通知。



首先,我们定义一个Windows消息,告诉系统,当有客户端数据到来时,发送该消息给我们。

#define  UM_SOCK_ASYNCRECVMSG  WM_USER + 1


消息处理函数

LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam); 


wParam参数携带的是SOCKET。

这时候分两种情况。

a.当接收到FD_ACCEPT网络事件的时候,参数wParam是server socket 
b.当接收到其它事件的时候,参数wParma是client socket

这样我们就不用自己判断是谁发来的消息,直接accept(wparam…..),recv(wParam…..),send(wParam……)就行了


lParam参数高字节携带的是错误信息,低字节携带的是网络事件

其宏定义如下:

#define WSAGETSELECTERROR(lParam)           HIWORD(lParam)#define WSAGETSELECTEVENT(lParam)           LOWORD(lParam)
  • 1
  • 2
  • 1
  • 2

最后根据WSAGETSELECTEVENT(lParam)来确定是哪个网络事件被触发。


注意:应用程序在调用WSAAsyncSelect函数后,套接字就被设置为非阻塞状态。而使用select函数不改变套接字的工作方式。如果调用了像WSARecv这样的Winsock函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。


服务端(   工程-设置      /subsystem:windows  )


#include <WINSOCK2.H> /*#include <windows.h>*/ #pragma comment(lib,"WS2_32") #define WM_SOCKET WM_USER+101 //----------------窗口过程函数的声明------------- LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam); //----------------WinMain()函数------------------ int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd ) {     WNDCLASS wc;     wc.style=CS_HREDRAW|CS_VREDRAW;     wc.lpfnWndProc=WindowProc;     wc.cbClsExtra=0;     wc.cbWndExtra=0;     wc.hInstance=hInstance;     wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);     wc.hCursor=LoadCursor(NULL,IDC_ARROW);    HBRUSH  hbrush = CreateSolidBrush( RGB(255,0,0));       //wc.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);     wc.hbrBackground=hbrush;     wc.lpszMenuName=NULL;     wc.lpszClassName="Test";     //---注册窗口类----     RegisterClass(&wc);     //---创建窗口----     HWND hwnd=CreateWindow("Test","窗口标题",WS_SYSMENU,300,0,600,400,NULL,NULL,hInstance,NULL);     if (hwnd==NULL)     {         MessageBox(hwnd,"创建窗口出错","标题栏提升",MB_OK);         return 1;     }     //---显示窗口----     ShowWindow(hwnd,SW_SHOWNORMAL);     UpdateWindow(hwnd);     //---socket-----     WSADATA wsaData;     WORD wVersionRequested=MAKEWORD(2,2);     if (WSAStartup(wVersionRequested,&wsaData)!=0)     {         MessageBox(NULL,"WSAStartup() Failed","调用失败",0);         return 1;     }     SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);     if (s==INVALID_SOCKET)     {         MessageBox(NULL,"socket() Failed","调用失败",0);         return 1;     }         sockaddr_in sin;     sin.sin_family=AF_INET;     sin.sin_port=htons(6000);     sin.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");     if (bind(s,(sockaddr*)&sin,sizeof(sin))==SOCKET_ERROR)     {         MessageBox(NULL,"bind() Failed","调用失败",0);         return 1;     }     if (listen(s,3)==SOCKET_ERROR)     {         MessageBox(NULL,"listen() Failed","调用失败",0);         return 1;     }     else         MessageBox(hwnd,"进入监听状态!","标题栏提示",MB_OK);     WSAAsyncSelect(s,hwnd,WM_SOCKET,FD_ACCEPT|FD_CLOSE);     //---消息循环----     MSG msg;     while (GetMessage(&msg,0,0,0))     {         TranslateMessage(&msg);         DispatchMessage(&msg);     }     closesocket(s);     WSACleanup();     return msg.wParam; } //-------------------窗口过程---------------------- LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam) {     switch(uMsg)     {     case WM_SOCKET:         {             SOCKET ss=wParam;   //wParam参数标志了网络事件发生的套接口            long event = WSAGETSELECTEVENT(lParam); // 事件                int error = WSAGETSELECTERROR(lParam); // 错误码                if ( error )             {                 closesocket(ss);                 return 0;             }             switch ( event )             {             case FD_ACCEPT:   //-----①连接请求到来                 {                      sockaddr_in Cadd;                      int Cadd_len=sizeof(Cadd);                     SOCKET sNew=accept(ss,(sockaddr*)&Cadd,&Cadd_len);                     if (ss==INVALID_SOCKET)                                             MessageBox(hwnd,"调用accept()失败!","标题栏提示",MB_OK);                          WSAAsyncSelect(sNew,hwnd,WM_SOCKET,FD_READ|FD_CLOSE);                 }break;             case FD_READ:   //-----②数据发送来                 {                     char cbuf[256];                     memset(cbuf,0,256);                     int cRecv=recv(ss,cbuf,256,0);                     if ((cRecv==SOCKET_ERROR&& WSAGetLastError() == WSAECONNRESET)|| cRecv==0)                     {                         MessageBox(hwnd,"调用recv()失败!","标题栏提示",MB_OK);                         closesocket(ss);                     }                     else if (cRecv>0)                     {                         MessageBox(hwnd,cbuf,"收到的信息",MB_OK);                             char Sbuf[]="Hello client!I am server";                         int isend=send(ss,Sbuf,sizeof(Sbuf),0);                         if (isend==SOCKET_ERROR || isend<=0)                         {                             MessageBox(hwnd,"发送消息失败!","标题栏提示",MB_OK);                                                     }                         else                             MessageBox(hwnd,"已经发信息到客户端!","标题栏提示",MB_OK);                     }                 }break;             case FD_CLOSE:    //----③关闭连接                 {                     closesocket(ss);                 }                 break;             }         }         break;     case WM_CLOSE:         if (IDYES==MessageBox(hwnd,"是否确定退出?","message",MB_YESNO))             DestroyWindow(hwnd);         break;     case WM_DESTROY:         PostQuitMessage(0);         break;     default:         return DefWindowProc(hwnd,uMsg,wParam,lParam);     }     return 0; } 



客户端

#include<stdlib.h>#include<WINSOCK2.H> #include <windows.h> #include <process.h>  #include<iostream>#include<string>using namespace std;#define BUF_SIZE 64#pragma comment(lib,"WS2_32.lib")void recv(PVOID pt)  {      SOCKET  sHost=  *((SOCKET *)pt);      while(true){    char buf[BUF_SIZE];//清空接收数据的缓冲区memset(buf,0 , BUF_SIZE);int retVal=recv(sHost,buf,sizeof(buf),0);if(SOCKET_ERROR==retVal){int  err=WSAGetLastError();//无法立即完成非阻塞Socket上的操作if(err==WSAEWOULDBLOCK){Sleep(1000);printf("\nwaiting  reply!");continue;}else if(err==WSAETIMEDOUT||err==WSAENETDOWN|| err==WSAECONNRESET)//已建立连接{printf("recv failed!");closesocket(sHost);WSACleanup();return  ;}}Sleep(100);        printf("\n%s", buf); //break;} }  int main(){WSADATA wsd;SOCKET sHost;SOCKADDR_IN servAddr;//服务器地址int retVal;//调用Socket函数的返回值char buf[BUF_SIZE];//初始化Socket环境if(WSAStartup(MAKEWORD(2,2),&wsd)!=0){printf("WSAStartup failed!\n");return -1;}sHost=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//设置服务器Socket地址servAddr.sin_family=AF_INET;servAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中servAddr.sin_port=htons(6000);//计算地址的长度int sServerAddlen=sizeof(servAddr); //调用ioctlsocket()将其设置为非阻塞模式   int iMode=1;retVal=ioctlsocket(sHost,FIONBIO,(u_long FAR*)&iMode); if(retVal==SOCKET_ERROR){printf("ioctlsocket failed!");WSACleanup();return -1;}//循环等待while(true){//连接到服务器retVal=connect(sHost,(LPSOCKADDR)&servAddr,sizeof(servAddr));if(SOCKET_ERROR==retVal){int err=WSAGetLastError();//无法立即完成非阻塞Socket上的操作if(err==WSAEWOULDBLOCK||err==WSAEINVAL){Sleep(1);printf("check  connect!\n");continue;}else if(err==WSAEISCONN)//已建立连接{break;}else{printf("connection failed!\n");closesocket(sHost);WSACleanup();return -1;}}}    unsigned long     threadId=_beginthread(recv,0,&sHost);//启动一个线程接收数据的线程   while(true){//向服务器发送字符串,并显示反馈信息printf("input a string to send:\n");std::string str;//接收输入的数据std::cin>>str;//将用户输入的数据复制到buf中ZeroMemory(buf,BUF_SIZE);strcpy(buf,str.c_str());if(strcmp(buf,"quit")==0){printf("quit!\n");break;}while(true){retVal=send(sHost,buf,strlen(buf),0);if(SOCKET_ERROR==retVal){int err=WSAGetLastError();if(err==WSAEWOULDBLOCK){//无法立即完成非阻塞Socket上的操作Sleep(5);continue;}else{printf("send failed!\n");closesocket(sHost);WSACleanup();return -1;}}break;}  }return 0;}


扩展讨论
WinSock在系统底层为套接字收发网络数据各提供一个缓冲区,接收到的网络数据会缓存在这里等待应用程序读取,待发送的网络数据也会先
写进这里之后通过网络发送。相关的,针对FD_READ和FD_WRITE事件的读写处理,因涉及的内容稍微复杂而容易使人困惑,这里需要特别进行
讨论。在FD_READ事件中,使用recv()函数读取网络包数据时,由于事先并不知道完整网络包的大小,所以需要多次读取直到读完整个缓冲区
。这就需要类似如下代码的调用:
void* buf = 0;    
int size = 0;    
while (true)    
{    
    char tmp[128];    
    int bytes = recv(socket, tmp, 128, 0);    
    if (bytes <= 0)    
        break;    
    else    
    {    
        int new_size = size + bytes;    
        buf = realloc(buf, new_size);    
        memcpy((void*)(((char*)buf) + size), tmp, bytes);    
        size = new_size;    
    }    
}    
//此时数据已经从缓冲区全部拷贝到buf中,你可以在这里对buf做一些操作      
free(buf);    
这一切看起来都没有什么问题,但是如果程序运行起来,你会收到比预期多出许多的FD_READ事件。如MSDN所述,正常的情况下,应用程序应
当为每一个FD_READ消息仅调用一次recv()函数。如果一个应用程序需要在一个FD_READ事件处理中调用多次recv(),那么它将会收到多个
FD_READ消息,因为每次未读完缓冲区的recv()调用,都会重新触发一个FD_READ消息。针对这种情况,我们需要在读取网络包前关闭掉FD_READ
消息通知,读取完这后再进行恢复,关闭FD_READ消息的方法很简单,只需要调用WSAAsyncSelect时参数lEvent中FD_READ字段不予设置即可。

//关闭FD_READ事件通知    
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE);    
// 读取网络包    
…    
// 再次打开FD_READ事件通知    
WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE | FD_READ);    

第二个需要讨论的是FD_WRITE事件。这个事件指明缓冲区已经准备就绪,有了多出的空位可以让应用程序写入数据以供发送。该事件仅在两种
情况下被触发:
1. 套接字刚建立连接时,表明准备就绪可以立即发送数据。
2. 一次失败的send()调用后缓冲区再次可用时。如果系统缓冲区已经被填满,那么此时调用send()发送数据,将返回SOCKET_ERROR,使用
WSAGetLastError()会得到错误码WSAEWOULDBLOCK表明被阻塞。这种情况下当缓冲区重新整理出可用空间后,会向应用程序发送FD_WRITE消息,
示意其可以继续发送数据了。

所以说收到FD_WRITE消息并不单纯地等同于这是使用send()的唯一时机。一般来说,如果需要发送消息,直接调用send()发送即可。如果该次
调用返回值为SOCKET_ERROR且WSAGetLastError()得到错误码WSAEWOULDBLOCK,这意味着缓冲区已满暂时无法发送,此刻我们需要将待发数据
保存起来,等到系统发出FD_WRITE消息后尝试重新发送。也就是说,你需要针对FD_WRITE构建一套数据重发的机制,文末的工程源码里包含有
这套机制以供大家参考,这里不再赘述。

结语
至此,如何在非阻塞模式下使用WinSock进行编程介绍完毕,这个框架可以满足大多数网络游戏客户端及部分服务器的通信需求。更多应用层面上的问题(如TCP粘包等)这里没有讨论。