WSAAsyncSelect 消息模型

select 模型虽然可以管理多个socket,但是它涉及到一个时机的问题,select模型会针对所管理的数组中的每一个socket循环检测它管理是否在对应的数组中,从时间复杂度上来说它是O(n^2)的,而且还有可能发生数组中没有socket处于待决状态而导致本轮循环做无用功的情况,针对这些问题,winsock中有了新的模型——WSAAsyncSelect 消息模型

消息模型的核心是基于Windows窗口消息获得网络事件的通知,Windows窗口是用来与用户交互的,而它并不知道用户什么时候会操作窗口,所以Windows窗口本身就是基于消息的异步通知,网络事件本身也是一个通知消息,将二者结合起来可以很好的使socket通知像消息那样当触发通知时调用窗口过程。这样就解决了select中的时机问题和里面两层循环的问题
WSAAsyncSelect函数原型如下:

1
2
3
4
5
6
int WSAAsyncSelect(
__in SOCKET s,
__in HWND hWnd,
__in unsigned int wMsg,
__in long lEvent
);

第一个参数是绑定的socket,第二个参数是消息所对应的窗口句柄,第三个参数是对应的消息,这个消息需要自己定义,第4个参数是我们所关心的事件,当在s这个socket发生lEvent这个事件发生时会向hWnd对应的窗口发送wMsg消息。
在消息附带的两个参数wParam和lParam中,lParam的高位16位表示当前的错误码,低16位表示当前socket上发生的事件。其中事件的取值如下:

  1. FD_WRITE : 当socket上可写时触发该事件,FD_WRITE的触发与调用send没有必然的联系,FD_WRITE只是表示socket已经为发送准备好了必要的条件,其实调用时可以不必理会这个事件,只需要在想发送数据的场合调用send,一般来说FD_WRITE只在这些条件下触发:a) 调用connect函数成功连接到服务器 b) 调用accept接受连接成功后(该条件是绑定在accept返回的那个与客户端通讯的socket上) c)调用send,sendto 失败并返回WSAWOULDBLOCK(由于是异步操作,可能同时客户端也在发数据, 此时可能导致send失败)
    为了方便我们处理这些参数,WinSock 提供了两个宏来解析它的高16位和低16位,分别是WSAGETSELECTERROR和WSAGETSELECTEVENT
    而lParam则保存了当前触发事件的socket句柄

如果对一个句柄调用了WSAAsyncSelect 并成功后,对应的socket会自动编程非阻塞模式。它就不像前面的select模型那样需要显示调用ioctrlsocket将socekt设置为非阻塞。
另外不需要每个socket都定义一个消息ID,通常一个ID已经足够处理所有的socket事件。
下面是一个具体的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
int _tmain(int argc, TCHAR *argv[])
{
WSADATA wd = {0};
WSAStartup(MAKEWORD(2, 2), &wd);
SOCKADDR_IN SrvAddr = {AF_INET};
SrvAddr.sin_addr.s_addr = htonl(INADDR_ANY);
SrvAddr.sin_port = htons(SERVER_PORT);

SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (INVALID_SOCKET == skServer)
{
printf("初始化socket失败,错误码为:%08x\n", WSAGetLastError());
goto __CLEAR_UP;
}

if (0 != bind(skServer, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR)))
{
printf("绑定失败,错误码为:%08x\n", WSAGetLastError());
goto __CLEAR_UP;
}

if (0 != listen(skServer, 5))
{
printf("监听失败,错误码为:%08x\n", WSAGetLastError());
goto __CLEAR_UP;
}

RegisterWindow();
CreateAndShowWnd();

g_uSockMsgID = RegisterWindowMessage(SOCKNOTIFY_MESSAGE);
WSAAsyncSelect(skServer, g_hMainWnd, g_uSockMsgID, FD_ACCEPT | FD_CLOSE);

MessageLoop();
__CLEAR_UP:
if (INVALID_SOCKET != skServer)
{
closesocket(skServer);
}
WSACleanup();
return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT lRes = 0;
switch (uMsg)
{
case WM_CLOSE:
{
CloseWindow(hwnd);
DestroyWindow(hwnd);
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps = {0};
BeginPaint(hwnd, &ps);
EndPaint(hwnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
if (uMsg == g_uSockMsgID)
{
lRes = ParseNotifyMessage(wParam, lParam);
}
lRes = DefWindowProc(hwnd, uMsg, wParam, lParam);
}

return lRes;
}

LRESULT ParseNotifyMessage(WPARAM wParam, LPARAM lParam)
{
WORD wNotify = WSAGETSELECTEVENT(lParam);
WORD wError = WSAGETSELECTERROR(lParam);

if (wNotify == FD_ACCEPT)
{
return OnAcceptMsg((SOCKET)wParam, lParam);
}else if (wNotify == FD_READ)
{
return OnReadMsg((SOCKET)wParam, lParam);
}

return 1;
}

LRESULT OnAcceptMsg(SOCKET s, LPARAM lParam)
{
SOCKADDR_IN AddrClient = {0};
int nAddrSize = sizeof(SOCKADDR);
SOCKET sClient = accept(s, (SOCKADDR*)&AddrClient, &nAddrSize);
printf("有客户端连接进来[%s:%u]\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port));
return WSAAsyncSelect(sClient, g_hMainWnd, g_uSockMsgID, FD_WRITE | FD_READ | FD_CLOSE);
}

LRESULT OnReadMsg(SOCKET s, LPARAM lParam)
{
char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024);
ZeroMemory(pszBuf, 1024);
int nTotalSize = 0;
int i = 1;

while (TRUE)
{
i++;
int nReadSize = recv(s, pszBuf + nTotalSize, 1024, 0);
if (nReadSize < 1024)
{
nTotalSize += nReadSize;
break;
}

nTotalSize += nReadSize;
HeapReAlloc(GetProcessHeap(), 0, pszBuf, 1024 * i);
}

if (strcmp(pszBuf, "exit") == 0)
{
shutdown(s, SD_BOTH);
closesocket(s);
}

send(s, pszBuf, nTotalSize, 0);
HeapFree(GetProcessHeap(), 0, pszBuf);

return 0;
}



在上面的代码中我们在main函数中创建了窗口程序,而常规的都是在WinMain中创建,其实从本质上讲控制台程序和窗口程序都是一个进程,至于以main作为入口还是以WinMain作为入口只是习惯上这样,但是并没有硬性规定。
在创建窗口之后我们将监听socket也绑定到窗口消息中,然后在对应的消息中判断FD_ACCEPT事件,如果是则调用accept进行连接。并将对生成的socket进行绑定。
在接下来的socket消息中主要处理FD_READ事件,当发生READ事件时调用read接收数据,然后调用send将数据原封不动的发送出去。

从上面的代码上看,该模型相对于select来说省去了查看socket是否在对应数组中的操作,减少了循环。而且可以很好的把握什么调用时机问题。
主要的缺点是它需要一个窗口,这样在服务程序中基本就排除掉了这个模型,它基本上只会出现在客户端程序中。
另外如果在一个窗口中需要管理成千上万个句柄时,它的性能会急剧下降,因此它的伸缩性较差。但是在客户端中基本不存在这个问题,所以如果要在客户端中想要减少编程难度,它是一个不二的选择