异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O模型。利用这个模型,应用程序可在一个套接字上,接收以 Windows消息为基础的网络事件通知。具体
的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型的核心即是WSAAsyncSelect函数。
WSAAsyncSelect函数定义如下:
int WSAAsyncSelect(
__in SOCKET s, //指定的是我们感兴趣的那个套接字。
__in HWND hWnd, //指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。
__in unsigned intwMsg, //指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。
__in long lEvent //指定一个位掩码,对应于一系列网络事件的组合
);
注意:
1、wMsg参数指定的消息通常是我们自定义的消息,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗
口消息发生混淆与冲突。
2、lEvent参数指定的网络类型为:FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等。当然,到底使用FD_ACCEPT,还是使用
FD_CONNECT类型,要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算就OK。
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
3、多个事件务必在套接字上一次注册!另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对那
个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有
网络事件通知。
4、若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“锁定”变成“非锁定”。这样一来,如果调用了像WSARecv这样的Winsock函数,
但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的
用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。窗口例程通常
定义如下:
LRESULT CALLBACK WindowProc(
HWND hwnd, //指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。
UINT uMsg, //指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。
WPARAM wParam, //指定在其上面发生了一个网络事件的套接字。(假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。)
LPARAM lParam //包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何
//错误代码。
);
流程:网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用
它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,具体的做法便是读取lParam低字位的内容。此时可
使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。
FD_WRITE事件通知:只有在三种条件下,才会发出 FD_WRITE通知:
1、使用 connect或 WSAConnect,一个套接字首次建立了连接。
2、使用 accept或 WSAAccept,套接字被接受以后。
3、若 send、WSASend、sendto或 WSASendTo操作失败,返回了 WSAEWOULDBLOCK错误,而且缓冲区的空间变得可用。
#include <winsock.h>
#include <tchar.h>
#define PORT 5150
#define MSGSIZE 1024
#define WM_SOCKET WM_USER+0
#pragma comment(lib,"ws2_32.lib")
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance, PSTR szCmdLine,intiCmdShow)
{
static TCHAR szAppName[] = _T("AsyncSelect Model");
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW| CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass(&wndclass))
{
MessageBox (NULL, TEXT ("This program requires WindowsNT!"), szAppName,MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, // window class name
TEXT ("AsyncSelect Model"),// window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg) ;
DispatchMessage(&msg) ;
}
return msg.wParam;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAMlParam)
{
WSADATA wsd;
static SOCKET sListen;
SOCKET sClient;
SOCKADDR_IN local, client;
int ret, iAddrSize =sizeof(client);
char szMessage[MSGSIZE];
switch (message)
{
case WM_CREATE:
// Initialize Windows Socket library
WSAStartup(0x0202, &wsd);
// Create listening socket
sListen = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr =htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local,sizeof(local));
// Listen
listen(sListen, 3);
// Associate listening socket with FD_ACCEPT event
WSAAsyncSelect(sListen, hwnd,WM_SOCKET, FD_ACCEPT);
return 0;
case WM_DESTROY:
closesocket(sListen);
WSACleanup();
PostQuitMessage(0);
return 0;
case WM_SOCKET:
if(WSAGETSELECTERROR(lParam))
{
closesocket(wParam);
break;
}
switch(WSAGETSELECTEVENT(lParam))
{
caseFD_ACCEPT:
// Accept a connection from client
sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize);
// Associate client socket with FD_READ and FD_CLOSEevent
WSAAsyncSelect(sClient, hwnd,WM_SOCKET, FD_READ | FD_CLOSE);
break;
caseFD_READ:
ret = recv(wParam, szMessage,MSGSIZE, 0);
if (ret== 0 || ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)
{
closesocket(wParam);
}
else
{
szMessage[ret] ='\0';
send(wParam, szMessage,strlen(szMessage), 0);
}
break;
caseFD_CLOSE:
closesocket(wParam);
break;
}
return 0;
}
return DefWindowProc(hwnd, message, wParam,lParam);
}
在我看来,WSAAsyncSelect是最简单的一种Winsock I/O模型(之所以说它简单是因为一个主线程就搞定了)。使用Raw Windows API写过窗口类应用程序的人应该都能看得懂。这里,我们需要做的仅仅是:
1.在WM_CREATE消息处理函数中,初始化Windows Socketlibrary,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件;
2.自定义一个消息WM_SOCKET,一旦在我们所关心的套接字(监听套接字和客户端套接字)上发生了某个事件,系统就会调用WndProc并且message参数被设置为WM_SOCKET;
3.在WM_SOCKET的消息处理函数中,分别对FD_ACCEPT、FD_READ和FD_CLOSE事件进行处理;
4.在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除Windows Socketlibrary
下面这张用于WSAAsyncSelect函数的网络事件类型表可以让你对各个网络事件有更清楚的认识:
表1
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字“服务质量”(QoS)发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE 应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知
大体流程
自定义消息
#define WM_SOCKET WM_USER+1
//创建监听socket
socket
//绑定本地地址
bind
//监听
listen
//注册监听socket 上的accept事件
WSAAsyncSelect(sListen, hwnd,WM_SOCKET, FD_ACCEPT);
在消息处理函数中处理WM_SOCKET消息
case WM_SOCKET:
//先检查是否有错误
if(WSAGETSELECTERROR(lParam))
{
closesocket(wParam);
break;
}
//处理过来的事件
switch(WSAGETSELECTEVENT(lParam))
{
caseFD_ACCEPT:
//接收一个客户端socket
sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize);
//注册客户端socket的read和close事件
WSAAsyncSelect(sClient, hwnd,WM_SOCKET, FD_READ | FD_CLOSE);
break;
caseFD_READ:
break;
caseFD_CLOSE:
}
总结:
使用windows的消息机制,实现了监听socket的accept和客户端的read之间,以及各个客户端之间的read,可以不用一直阻塞在那,而是在有相应事件的时候再进行阻塞处理,通过消息机制消除了所有的长阻塞(select模型消除了accept和read的长阻塞,增加了一个select长阻塞),给程序腾出了自由的时间。