Winsock网络编程套接字I/O模型之WSAAsyncSelect 模型
第二章 WSAAsyncSelect 异步I/O模型
WSAAsyncSelect模型实际上是通过Windows消息机制接收网络事件通知。MFC(Microsoft Foundation Class,Microsoft 基础类库)中的CSocket 类也使用了它,这使得开发带 GUI 界面的网络程序变得很简单。但是如果连接增加,单个 Windows 函数处理上千个客户请求时,服务器性能势必会受到影响。
一、相关函数与数据结构
1. WSAAsyncSelect 函数
int WSAAsyncSelect(
__in SOCKET s, // 标识需要事件通知的套接字的描述符
__in HWND hWnd, // 标识在网络事件发生时接收消息的窗口的句柄
__in unsigned int wMsg, // 网络事件发生时接收的消息ID,可以在WM_USER的数值中选择一个用做ID
__in long lEvent // 位掩码,指定应用程序感兴趣(想要捕获)的网络事件的组合值(即取或的值)
);
(1)返回值
如果WSAAsyncSelect函数成功,返回值为0。否则,返回SOCKET_ERROR值,并且可以通过调用WSAGetLastError取得特定的错误编号:
错误码 | 含义 |
---|---|
WSANOTINITIALISED | 在使用这个函数之前,必须成功调用WSAStartup |
WSAENETDOWN | 网络子系统故障 |
WSAEINVAL | 指定的参数之一无效,例如窗口句柄没有引用现有的窗口,或者指定的套接字处于无效状态 |
WSAEINPROGRESS | 在使用这个函数之前,必须成功调用WSAStartup |
WSAENOTSOCK | 描述符不是套接字 |
当窗口接收到消息时,可以设置额外的错误码。使用WSAGETSELECTERROR宏从应答消息中的lParam中提取此错误代码。不同网络事件错误码不同,需要时请查看MSDN Library中Windows Sockets Error Codes和WSAAsyncSelect。
(2)参数lEvent
- WSAAsyncSelect函数在检测到由lEvent参数指定的网络事件时,向窗口hWnd发送一个消息。无论lEvent的值是多少,WSAAsyncSelect函数自动设置套接字s为非阻塞模式。如果要将套接字s设置回阻塞模式,首先需要通过调用WSAAsyncSelect将lEvent设置为0来清除与套接字s相关的事件记录。然后可以调用ioctlsocket或WSAIoctl将套接字设置回阻塞模式。有关如何将非阻塞套接字设置回阻塞模式的更多信息,请查询ioctlsocket和WSAIoctl函数。
- lEvent参数是通过对下表中列出的任何值使用按位或操作符来构造的:
值 | 含义 |
---|---|
FD_READ | 设置接收准备读的通知。套接字接收到对方发送过来的数据包,表明这时可以去读套接字了 |
FD_WRITE | 希望收到准备写的通知。数据缓冲区满后再次变空时,WinSock 接口通过该通知码通知应用程序,可以继续发送数据了(短时间内发送数据过多,便会造成数据缓冲区变满) |
FD_OOB | 希望接收OOB数据到达的通知 |
FD_ACCEPT | 希望接收即将到来的连接通知。监听中的套接字检测到有连接进入 |
FD_CONNECT | 希望接收完成连接或多点连接操作的通知。如果用套接字去连接对方的主机,当连接动作完成以后会接收到这个通知码 |
FD_CLOSE | 希望收到socket关闭的通知。检测到套接字对应的连接被关闭 |
FD_QOS | 希望接收套接字服务质量(QOS)更改的通知 |
FD_GROUP_QOS | 希望接收套接字组服务质量更改的通知(保留供将来与套接字组一起使用) |
FD_ROUTING_INTERFACE_CHANGE | 希望接收指定目的地址的路由接口更改通知 |
FD_ADDRESS_LIST_CHANGE | 希望接收socket协议族的本地地址列表更改的通知 |
- 对套接字执行WSAAsyncSelect会取消之前对同一个套接字执行的WSAAsyncSelect或WSAEventSelect(后面的章节会介绍这个函数),也就是说如果先后对同一个套接字调用了WSAAsyncSelect,那么这个套接字最终的效果会按照最新的一次调用所设置的参数为准。
(3)调用示例
::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE); // WM_SOCKET 为自定义消息
上述代码将套接字 sListen 设为窗口通知消息类型。WM_SOCKET 为自定义网络通知消息,FD_CLOSE|FD_ACCEPT 指定了 sListen 套接字只接收 FD_CLOSE 和 FD_ACCEPT 通知消息。当有客户连接或套接字关闭时,Winsock 接口将向指定的窗口发送 WM_SOCKET 消息。
2. 窗口回调函数
LRESULT CALLBACK WindowProc(
HWND hWnd, // 窗口句柄
UINT uMsg, // 消息ID
WPARAM wParam, // 指定发生网络事件的套接字句柄
LPARAM lParam // 低字位指定了发生的网络事件,高字位包含了任何可能出现的
);
可以使用 Winsock2.h 中的宏 WSAGETSELECTERROR 和 WSAGETSELECTEVENT 将 lParam 中的数据取出。
#define WSAGETSELECTERROR(lParam) HIWORD(lParam) // 高字为出错代码
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam) // 低字为通知码
如果没有错误发生,错误代码为 0,程序可以继续循环检查通知码,以确定发生的网络事件。
二、完整代码实现
下面例子是一个 TCP 服务器程序,接受客户端的连接请求,打印出接收到的数据。
#include "../common/initsock.h"
#include <stdio.h>
#define WM_SOCKET WM_USER + 101 // 自定义消息
CInitSock theSock; // 初始化Winsock库
// 声明窗口回调函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int main()
{
char szClassName[] = "MainWClass"; // 要注册的窗口类名
WNDCLASSEX wndclass;
// 用描述主窗口的参数填充WNDCLASSEX结构
wndclass.cbSize = sizeof(wndclass);
wndclass.style = CS_HREDRAW|CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc; // 指定窗口回调函数地址
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = NULL;
wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szClassName;
wndclass.hIconSm = NULL;
::RegisterClassEx(&wndclass);
// 创建主窗口
HWND hWnd = ::CreateWindowEx(
0,
szClassName,
"",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
NULL,
NULL);
if(hWnd == NULL)
{
::MessageBox(NULL, "创建窗口出错!", "error", MB_OK);
return -1;
}
USHORT nPort = 2356; // 此服务器监听的端口号
// 创建监听套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定套节字到本地机器
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() \n");
return -1;
}
// 将服务器的套接字设为窗口通知消息类型,这里指定接受的网络事件类型表示当有客户端连接时和已建立的连接关闭时都会通知
::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
// 进入监听模式
::listen(sListen, 5);
// 从消息队列中取出消息
MSG msg;
while(::GetMessage(&msg, NULL, 0, 0))
{
// 转化键盘消息
::TranslateMessage(&msg);
// 将消息发送到相应的窗口函数
::DispatchMessage(&msg);
}
// 当GetMessage返回0时程序结束
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SOCKET:
{
// 取得有事件发生的套节字句柄
SOCKET s = wParam;
// 查看是否出错
if(WSAGETSELECTERROR(lParam))
{
::closesocket(s);
return 0;
}
// 处理发生的事件
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT: // 监听中的套接字检测到有连接进入
{
SOCKET client = ::accept(s, NULL, NULL);
// 将已连接的客户端套接字设为窗口通知消息类型
::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
}
break;
case FD_WRITE:
{
// 服务器不主动发送数据,所以这里没有做处理
}
break;
case FD_READ:
{
char szText[1024] = { 0 };
if(::recv(s, szText, 1024, 0) == -1)
::closesocket(s);
else
printf("接收数据:%s", szText);
}
break;
case FD_CLOSE:
{
::closesocket(s);
}
break;
}
}
return 0;
case WM_DESTROY: // 窗口销毁消息
::PostQuitMessage(0) ;
return 0 ;
}
// 将我们不处理的消息交给系统做默认处理
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
三、总结
为什么实现了异步管理连接的套接字:通过使用异步函数WSAAsyncSelect,服务器可以注册网络事件的通知。当网络事件发生时,操作系统会通知服务器,从而避免了阻塞在单个连接的recv函数上,服务器可以继续接收其他连接请求,而无需等待上一个连接上的网络事件完成,也不需要创建多线程处理这些连接。
缺点:用单个线程的消息队列管理大规模的客户端连接产生的不同网络事件,可能会导致消息队列中事件排队等待时间变长。
参考文献
《Windows 网络与通信程序设计》
MSDN Library