转载自:http://blog.csdn.net/ithzhang/article/details/8464330
WSAAsyncSelect模型是Windows socket的一个异步IO模型。利用该模型可以接收以Windows消息为基础的网络事件。Windows sockets应用程序在创建套接字后,调用WSAAsyncSelect函数注册感兴趣的网络事件,当该事件发生时Windows窗口收到消息,应用程序就可以对接收到的网络时间进行处理。
WSAAsyncSelect是select模型的异步版本。在应用程序使用select函数时会发生阻塞现象。可以通过select的timeout参数设置阻塞的时间。在设置的时间内,select函数等待,直到一个或多个套接字满足可读或可写的条件。
而WSAAsyncSelect是非阻塞的。Windows sockets程序在调用recv或send之前,调用WSAAsyncSelect注册网络事件。WSAAsyncSelect函数立即返回。当系统中数据准备好时,会向应用程序发送消息。此此消息的处理函数中可以调用recv或send进行接收或发送数据。
WSAAsyncSelect模型与select模型的相同点是它们都可以对多个套接字进行管理。但它们也有不小的区别。首先WSAAsyncSelect模型是异步的,且通知方式不同。更重要的一点是:WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口,而select模型可以广泛应用在Unix系统,使用该模型不需要创建窗口。最后一点区别:应用程序在调用WSAAsyncSelect函数后,套接字就被设置为非阻塞状态。而使用select函数不改变套接字的工作方式。
该函数告诉系统当网络事件发生时为套接字发送消息。声明如下:
int WSAAsyncSelect(SOCKET s, HWND hWnd, u_int wMsg, long lEvent);
s 为需要通知的套接字。
hWnd 为当网络事件发生时接收消息的窗口句柄。
wMsg 为当网络事件发生时窗口收到的消息。在此消息的响应函数内对网络事件进行处理。
lEvent 为应用程序感兴趣的网络事件集合。
应用程序调用该函数后自动将套接字设置为非阻塞模式。通常用户自定义消息应该在WM_USER的基础之上定义。如WM_USER+1,以避免与Windows预定义的消息发生混淆。
网络事件可以有以下几种:
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 :本地地址列表发生变化的通知。
开发人员应向应用程序注册感兴趣的网络事件。可以将它们按位或并传给lEvent函数。如:
WSAAsyncSelect(s, hWnd, WM_SOCKET, FD_CONNECT | FD_READ | FD_CLOSE);
上述代码表示:当套接字连接到来、有数据可读或这套接字关闭的网络事件发生时,WM_SOCKET消息就会发送给hWnd为句柄的窗口。
消息处理函数。
消息处理函数是对网络事件发生时窗口消息的处理。它的声明如下:
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
// hWnd 为窗口句柄。
// uMsg 为当网络事件发生时的消息。
// wParam 为消息参数。该参数表明发生网络事件的套接字。
// lParam 也为消息参数。低字节表明已发生的网络事件。高字节包含错误代码。
在Windows sockets应用程序中,当WindowProc接收到网络消息时,在该函数内执行下面的步骤:
WSAGETSElECTERROR 和 WSAGETSELECTEVENT 宏定义如下:
#define WSAGETSElECTERROR(lParam) LOWORD(lParam)
#define WSAGETSELECTEVENT(lParam) HIWORD(lParam)
接下来就需要创建窗口和将网络消息与消息处理函数关联起来。如果使用MFC可以使用MFC提供的宏来进行处理。
注意:多次调用WSAAsyncSelect时,最后一次调用会取消前面注册的网络事件。
因为调用 accept 接受的套接字和监听套接字具有同样的属性。所以,任何为监听套接字设置的网络事件对接受套接字同样起作用。如果一个监听套接字请求 FD_ACCEPT、FD_READ 和 FD_WRITE 网络事件。则在该监听套接字上接受的任何套接字也会请求 FD_ACCEPT,FD_READ 和 FD_WRITE 网络事件。
FD_CLOSE 网络事件用来判断套接字是否已经关闭。错误代码会指出套接字是从容关闭还是硬关闭。如果为 0,为从容关闭。若错误代码为 WSAECONNRESET,则套接字是硬关闭。调用 closesocket 不会投递 FD_CLOSE 事件。
发生网络事件的条件:
下列条件下会发生 FD_READ 事件:
下列情况下会发生 FD_WRITE 事件:
优势:该模型是在基于消息的 Windows 环境下开发应用程序。开发人员可以像处理其他消息一样,对网络事件进行处理。而且为确保接受所有数据提供了很好的机制。
不足:由于该模型基于 Windows 消息机制,必须在应用程序中创建窗口。虽然可以在开发中,确定是否显示该窗口。 由于调用 WSAAsyncSelect 函数后自动将套接字设置为非阻塞状态,当应用程序接收到网络事件时,未必能够成功返回。这无疑增加了使用该模型的难度。
接下来展示一个使用如何 WSAAsyncSelect 模型的例子。该程序使用 WSAAsyncSelect 模型管理接受的客户端套接字。编码步骤如下:
1. 声明自定义消息。如 WM_SOCKET
2. 声明窗口例程。
3. 将自定义消息与消息处理函数相关联。
4. 初始化套接字动态库,创建套接字。
5. 调用 WSAAsyncSelect 注册感兴趣的网络事件。本例服务器感兴趣的网络事件有 FD_ACCEPT 和 FD_CLOSE。
6. 绑定套接字开始监听。
#define WM_SOCKET WM_USER+1 //套接字消息。
除了声明自定义消息外还需要声明最大字符串长度、服务器监听端口、数据缓冲区:
#define MAX_STRING 100 //最大字符串长度。
#define SERVERPORT 5000 //服务器端口。
#define MAX_SIZE_BUF 1024 //数据缓冲区长度。
1:在窗口类头文件中声明消息处理函数。如:
afx_msg LRESULT onWmSocket(WPARAM wParam, LPARAM lParam);
2:在消息映射宏中将自定义消息如声明的消息处理函数关联:
ON_MESSAGE(WM_SOCKET, &onWmSocket)
3:实现消息处理函数:
LRESULT CuserdefinedMessageTestDlg::onWmSocket(WPARAM wParam, LPARAM lParam)
{
if(WSAGETSELECTERROR(lParam))
{
//wParam为发生消息的套接字。出现错误,则从链表中将该套接字对应的CClient类对象删除。
m_list.deleteNode(wParam);
return false;
}
else
{
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT: //接受客户端连接请求。
{
SOCKET sAccept;
if((sAccept == accept(wParam, NULL, NULL) == INVALID_SOCKET))
{
break;
}
m_list.add(sAccept);
//在新接受的套接字发生FD_READ,FD_WRITE,FD_CLOSE网络事件,发送WM_SOCKET消息;
WSAAsyncSelect(sAccept, this->m_hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
}
break;
case FD_READ: //可读,接收数据。
{
CClient *pClinet = GetClient(wParam); //根据套接字,获取客户端节点。
pClient->RecvData();
}
break;
case FD_WRITE: //可写,发送数据。
{
CClient*pClient = GetClient(wParam);
pClient->SendData();
}
break;
case FD_CLOSE: //对方关闭套接字连接。
{
if(WSAGETSELECTERROR(lParam) == 0)
{
//从容关闭。
}
else if(WSAGETSELECTERROR(lParam) == WSAECONNREFUSED)
{
//硬关闭。
}
m_list.deleteNode(wParam);
}
break;
default:
break;
}
}
return 0;
}
1:初始化套接字动态库,并创建套接字:
WSADATA wsa;
int ret = WSAStartup(MAKEWORD(2, 2), &wsa);
ListenSocket = socket(AF_INET, SOCK_STREAM, 0);
if(ListenSocket == INVALID_SOCKET)
{
return false;
}
2:注册感兴趣的网络事件:
WSAAsyncSelect(ListenSocket, this, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
3:绑定套接字并监听:
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100");
addr.sin_port = htons(4000);
int ret = bind(m_ListenSocket, (SOCKADDR*)&addr, sizeof(addr));
if(ret == INVALID_SOCKET)
{
return false;
}
ret = listen(m_ListenSocket, 10);
if(ret == INVALID_SOCKET)
{
return false;
}
closesocket(m_ListenSocket);
WSACleanup();
自定义类CClient类用于管理服务器接受客户端的新建套接字。在该类中实现与客户端通信。
当服务器接受一个客户端连接请求后就会创建一个CClient实例。将该实例地址加入链表中。
以上参考自《精通Windows sockets网络开发–基于Visual C++实现》孙海民著。如有纰漏,请不吝指正!
2012.1.4 于山西大同