介绍

WSAAsyncSelect模型又称为异步选择模型,是为了适应Windows的消息驱动环境而设置的。引入Berkeley套接字后,该模型在Windows Sockets1.1版本中增加,以Windows系统中最常用的消息机制来反馈网络I/O事件,达到了收发数据的异步通知效果。这种模型在现在许多性能要求不高的网络应用程序中应用广泛,MFC中的CSocket类也使用了它。

WSAAsyncSelect模型的相关函数

WSAAsyncSelect模型的核心是WSAAsyncSelect()函数,它自动将套接字设置为非阻塞模式,使Windows应用程序能够接收网络事件消息。该函数的定义如下:

  1. int WSAAsyncSelect(
  2. __in SOCKET s,
  3. __in HWND hWnd,
  4. __in unsigned int wMsg,
  5. __in long lEvent
  6. );
  • s:关心某网络事件的套接字;
  • hWnd:网络事件发生时用于接收消息的窗口句柄;
  • wMsg:网络事件发生时接收到的消息;
  • lEvent:套接字感兴趣的网络事件。

WSAAsyncSelect()函数调用后,当检测到lEvent参数指定的网络事件发生时,Windows Sockets实现会向hWnd指定的窗体发送消息wMsg。如果函数成功,则返回0;否则返回SOCKET_ERROR。
参数lEvent可以包含的网络事件如表:Image00084.jpg
如果对多个网络事件都关心,则可以将以上的事件执行按位或操作,例如,应用程序希望接收到读就绪和连接关闭的通知事件,则对WSAAsyncSelect()函数的lEvent参数同时使用FD_READ和FD_CLOSE事件,代码示例如下:

  1. iResult = WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_CLOSE);

这里要注意对同一个套接字的多次WSAAsyncSelect()调用之间的影响。当我们对同一个套接字执行一次WSAAsyncSelect()时,会取消之前对该套接字的WSAAsyncSelect()或WSAEventSelect()调用所声明的网络事件。例如,下面的代码两次调用WSAAsyncSelect()函数,先后为套接字s的不同事件指定了不同的消息:

  1. iResult = WSAAsyncSelect(s, hWnd, wMsg1, FD_READ);
  2. iResult = WSAAsyncSelect(s, hWnd, wMsg2, FD_WRITE);

其结果是,第二次调用WSAAsyncSelect()函数会取消第一次调用对FD_READ事件的注册,也就是说,只有当FD_WRITE事件发生时,套接字s才会收到wMsg2指定的消息,wMsg1消息不会再发出。
因此,对同一个套接字而言,不能为不同的事件指定不同的消息,如果关注多个事件,需要用按位或的操作来实现。
如果要取消指定套接字上的所有通知事件,则可以在调用WSAAsyncSelect()函数时将事件参数lEvent设置为0,例如:

  1. iResult = WSAAsyncSelect(s, hWnd, 0, 0);

WSAAsyncSelect模型的编程框架

WSAAsyncSelect模型为套接字关心的网络事件绑定用户自定义的消息,当网络事件发生时,相应的消息被发送给关心该事件的套接字所在的窗口,从而使应用程序可以对该事件做相应的处理。

在消息驱动下,原来顺序执行的程序改为由两个相对独立的执行部分构成:

  • 主程序框架。这部分主要完成套接字的创建和初始化的工作,根据程序工作的环境不同,主程序框架中的功能可能会有一些差别。如果是基于SDK的应用程序,为了对消息队列进行创建和维护,需要创建维护消息资源的窗口并对消息进行循环转换和分发;如果是基于MFC的应用程序,MFC已对消息映射机制进行了合理的封装,主程序框架部分不需要手工维护消息队列。

  • 消息处理框架。在网络事件发生后,消息产生,该消息被主程序框架捕获到,消息处理框架部分被执行,主要完成消息类型的判断和网络事件的具体处理。在基于SDK的应用程序中,这部分功能是在窗口过程中完成的,窗口过程只有一个;而在基于MFC的应用程序中,这部分功能是在独立的消息处理函数中完成的,且消息处理函数可能会存在多个。

image.png
image.png

整体来看,基于WSAAsyncSelect模型的网络应用程序的基本流程如下

  1. 定义套接字网络事件对应的用户消息。
  2. 如果不存在窗口,则创建窗口和窗口例程支持函数。
  3. 调用WSAAsyncSelect()函数为套接字设置网络事件、用户消息和消息接收窗口之间的关系。
  4. 增加消息循环的具体功能,或者添加消息与消息处理函数的映射关系。
  5. 添加消息处理框架的具体功能,判断是哪个套接字上发生了网络事件。使用WSAGETSELECTEVENT宏了解所发生的网络事件,从而进行相应的处理。

依赖于不同的消息实现机制,在WSAAsyncSelect模型下应用程序的处理过程有较大的差别,我们分SDK环境和MFC环境两种情况举例说明套接字的创建及接收的基本过程。
以下示例实现了基于WSAAsyncSelect模型的面向连接通信服务器,该服务器的主要功能是接收客户使用TCP协议发来的数据,打印接收到的数据的字节数。

在SDK下实现基于WSAAsyncSelect模型的套接字通信服务器

  1. #include <WinSock2.h>
  2. #include <Windows.h>
  3. #include <WS2tcpip.h>
  4. #include <stdlib.h>
  5. #include <stdio.h>
  6. #include <conio.h>
  7. #include<tchar.h>
  8. #pragma comment(lib,"ws2_32.lib")
  9. #define WM_SOCKET WM_USER+101
  10. #define DEFAULT_BUFLEN 512
  11. #define DEFAULT_PORT 27015
  12. LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
  13. HWND CreateWorkWindow(void) {
  14. WNDCLASS wndclass;
  15. TCHAR ClassName[] = L"SelectWindow";//窗口类名
  16. HWND hWnd = NULL;//新建窗口句柄
  17. /*
  18. wndclass.style = CS_HREDRAW | CS_VREDRAW;//窗口类型
  19. wndclass.lpfnWndProc = (WNDPROC)WndProc;//指定窗口处理程序
  20. wndclass.cbClsExtra = 0;//窗口类结构体后面额外分配的字节
  21. wndclass.cbWndExtra = 0;//窗口实例后面额外分配的字节数
  22. wndclass.hInstance = NULL;//窗口类对应实例的句柄
  23. wndclass.hIcon = LoadIcon(NULL, IDC_ARROW);
  24. //光标句柄
  25. wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  26. // 刷子句柄(白色)
  27. wndclass.lpszMenuName = NULL; // 窗口中无菜单
  28. wndclass.lpszClassName = (WCHAR*)ClassName; // 窗口类名为"SelectWindow"
  29. */
  30. wndclass.style = CS_HREDRAW | CS_VREDRAW;
  31. wndclass.lpfnWndProc = WndProc;
  32. wndclass.cbClsExtra = 0;
  33. wndclass.cbWndExtra = 0;
  34. wndclass.hInstance = NULL;
  35. wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  36. wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
  37. wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  38. wndclass.lpszMenuName = NULL;
  39. wndclass.lpszClassName = ClassName;
  40. if (RegisterClass(&wndclass)==0)
  41. {
  42. printf("REGISTERCLASS() failed with error code %d\n",GetLastError());
  43. return NULL;
  44. }
  45. hWnd = CreateWindow(ClassName, L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL);
  46. if (hWnd==NULL)
  47. {
  48. printf("CREATEWINDOWS FAILD WITH ERROR CODE %d\n",GetLastError());
  49. return NULL;
  50. }
  51. return hWnd;
  52. }

输入参数:无。
输出参数:窗口句柄。
● 非NULL:表示成功;
● NULL:表示失败。
程序创建窗口时,可以创建预先定义的窗口类或自定义的窗口类。创建自定义的窗口类时,在使用该窗口类前必须注册该窗口类。使用RegisterClass()函数注册窗口类。
第5~21行代码创建了一个窗口结构WNDCLASS,该结构包含了RegisterClass()函数注册的类属性,并调用RegisterClass()函数对窗口结构进行注册。
第22~35行代码调用CreateWindow()函数创建接收消息的窗口。
(2)第二步:定义窗口例程
窗口例程具体实现消息处理框架的
窗口例程具体实现消息处理框架的功能,取出消息,判断消息类型,进行相应的网络处理。
该函数的具体实现代码如下:

  1. int main() {
  2. HWND hWnd;
  3. MSG Msg;
  4. if ((hWnd=CreateWorkWindow())==NULL)
  5. {
  6. printf("create window failed error code %d\n", GetLastError());
  7. return 0;
  8. }
  9. WSADATA wsaData;
  10. int iResult;
  11. SOCKET ServerSocket = INVALID_SOCKET;
  12. iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
  13. if (iResult!=0)
  14. {
  15. printf("WSASTART fAILED WITH ERROR CODE %d\n",iResult);
  16. WSACleanup();
  17. return 1;
  18. }
  19. ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
  20. if (ServerSocket == INVALID_SOCKET)
  21. {
  22. printf("socket failed with error: %ld\n", WSAGetLastError());
  23. WSACleanup();
  24. return 1;
  25. }
  26. SOCKADDR_IN addrServ;
  27. addrServ.sin_family = AF_INET;
  28. addrServ.sin_port = htons(DEFAULT_PORT);
  29. addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
  30. iResult = bind(ServerSocket, (const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
  31. if (iResult == SOCKET_ERROR)
  32. {
  33. printf_s("bind falied with error code %d\n", WSAGetLastError());
  34. closesocket(ServerSocket);
  35. WSACleanup();
  36. return 1;
  37. }
  38. iResult = listen(ServerSocket, SOMAXCONN);
  39. if (iResult == SOCKET_ERROR)
  40. {
  41. printf_s("listen failed with error code %d", iResult);
  42. closesocket(ServerSocket);
  43. WSACleanup();
  44. return -1;
  45. }
  46. printf_s("TCP server starting");
  47. WSAAsyncSelect(ServerSocket,hWnd,WM_SOCKET,FD_ACCEPT);
  48. while (GetMessage(&Msg,NULL,0,0))
  49. {
  50. TranslateMessage(&Msg);
  51. DispatchMessage(&Msg);
  52. }
  53. return 0;
  54. }

HWND hwnd:窗口句柄;
● UINT uMsg:消息;
● WPARAM wParam:消息传入参数,此处传入的是套接字;
● LPARAM lParam:消息传入参数,此处传入的是事件类型。
输出参数:
● TRUE:表示成功;
● FALSE:表示失败。
窗口例程是一个回调函数,网络事件到达后,窗口例程被执行。
第12~13行代码首先检查lParam参数的高位,以判断是否在套接字上发生了网络错误,宏WSAGETSELECTERROR返回高字节包含的错误信息。
第16~59行代码使用宏WSAGETSELECTEVENT读取lParam参数的低位确定发生的网络事件。本示例对FD_ACCEPT、FD_READ、FD_CLOSE三种常见的网络事件都进行了判断和处理。如果是FD_ACCEPT事件发生,则调用accept()函数返回新的连接套接字,并通过WSAAsyncSelect()函数注册该套接字上关心的网络事件;如果是FD_READ事件发生,则在发生事件的套接字上接收数据;如果是FD_CLOSE事件发生,则关闭相应的套接字;如果套接字还对其他网络事件感兴趣,则在swith-case语句中相应增加对其他事件的判断和处理。

(3)第三步:完成程序主框架
程序主框架实现了流式套接字服务器的主体功能,创建接收消息的窗口,初始化套接字,注册套接字感兴趣的网络事件和消息的对应关系,并不断地循环读取消息和分发消息。
该函数的具体实现代码如下:

  1. LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) {
  2. sockaddr_in addrClient;
  3. int addrClientLen = sizeof(sockaddr_in);
  4. char recvbuf[DEFAULT_BUFLEN];
  5. int recvbuflen = sizeof(recvbuf);
  6. SOCKET s, AcceptSocket;
  7. int iResult;
  8. if (uMsg== WM_SOCKET)
  9. {
  10. s = wParam;
  11. if (WSAGETSELECTERROR(lParam))
  12. {
  13. printf("socket failed error code %d\n", WSAGETSELECTERROR(lParam));
  14. }
  15. else
  16. {
  17. switch (WSAGETSELECTEVENT(lParam))
  18. {
  19. case FD_ACCEPT:
  20. //检测到有新的连接进入
  21. AcceptSocket = accept(s,(sockaddr FAR*)&addrClient,&addrClientLen);
  22. if (AcceptSocket==INVALID_SOCKET)
  23. {
  24. printf("ACCEPTED \n");
  25. closesocket(s);
  26. }
  27. printf("get new conn\n");
  28. WSAAsyncSelect(AcceptSocket,hWnd,WM_SOCKET,FD_READ|FD_WRITE|FD_CLOSE);
  29. break;
  30. case FD_READ:
  31. memset(recvbuf,0,recvbuflen);
  32. iResult = recv(s, recvbuf, recvbuflen, 0);
  33. if (iResult>=0)
  34. {
  35. printf("\nByte received %d\n", iResult);
  36. }
  37. else
  38. {
  39. printf("recv failed with error code %d",WSAGetLastError());
  40. closesocket(s);
  41. }
  42. case FD_WRITE:
  43. {}
  44. break;
  45. default:
  46. break;
  47. }
  48. }
  49. }
  50. return DefWindowProc(hWnd, uMsg, wParam, lParam);
  51. }

本程序是一个命令行程序,为了使用WSAAsyncSelect模型,需要用户手工创建窗口并维护消息队列。
第9行代码定义了套接字网络事件对应的用户消息WM_SOCKET。
第15~19行代码调用第一步声明的CreateWorkerWindow()函数,用于注册窗口并创建窗口。
第20~60行代码进行基于流式套接字的服务器程序初始化。
第62行代码调用WSAAsyncSelect()函数向窗口hWnd注册了套接字ServerSocket感兴趣的网络事件,并与用户自定义的消息WM_SOCKET关联起来。
第63~67行代码维护了一个消息循环,不断地通过GetMessage()函数取出消息,对消息转换后将消息发送给窗口例程。

在MFC下实现基于WSAAsyncSelect模型的套接字通信服务器

评价

WSAAsyncSelect模型的优点是在系统开销不大的情况下可以同时处理多个客户的网络I/O。但是,消息的运转需要有消息队列,通常消息队列依附于窗口实现,有些应用程序可能并不需要窗口,为了支持消息机制,就必须创建一个窗口来接收消息,这对于一些特殊的应用场合并不适合。另外,在一个窗口中处理大量的消息也可能成为性能的瓶颈