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参数高字节携带的是错误信息,低字节携带的是网络事件
其宏定义如下:
- 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粘包等)这里没有讨论。
- windows 下 WSAAsyncSelect模型
- Windows平台下WSAAsyncSelect 模型 服务器
- Windows socket 之WSAAsyncSelect模型
- Windows socket 之WSAAsyncSelect模型
- Windows socket 之WSAAsyncSelect模型
- Windows socket 之WSAAsyncSelect模型
- Windows socket 之WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect 模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- WSAAsyncSelect模型
- Android MediaPlayer的使用方法
- phpstrom DATABASE的使用问题
- 深入理解JVM(八)——类加载的时机
- Curl命令之详解
- 关于spring返回值Object
- windows 下 WSAAsyncSelect模型
- Mysql定时执行任务实现方法
- __type_traits
- elasticsearch之Document APIs【Delete By Query API】
- JSP 语法详解
- 听君一席话,胜读十年书
- 随软键盘弹出和收回view(二)
- Visual Studio2015产品密钥
- 将Mat矩阵图像绘制在DC上