windows 下 WSAAsyncSelect模型

      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粘包等)这里没有讨论。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页