select 模型雖然可以管理多個(gè)socket,但是它涉及到一個(gè)時(shí)機(jī)的問題,select模型會(huì)針對(duì)所管理的數(shù)組中的每一個(gè)socket循環(huán)檢測(cè)它管理是否在對(duì)應(yīng)的數(shù)組中,從時(shí)間復(fù)雜度上來說它是O(n^2)的,而且還有可能發(fā)生數(shù)組中沒有socket處于待決狀態(tài)而導(dǎo)致本輪循環(huán)做無用功的情況,針對(duì)這些問題,winsock中有了新的模型——WSAAsyncSelect 消息模型
消息模型的核心是基于Windows窗口消息獲得網(wǎng)絡(luò)事件的通知,Windows窗口是用來與用戶交互的,而它并不知道用戶什么時(shí)候會(huì)操作窗口,所以Windows窗口本身就是基于消息的異步通知,網(wǎng)絡(luò)事件本身也是一個(gè)通知消息,將二者結(jié)合起來可以很好的使socket通知像消息那樣當(dāng)觸發(fā)通知時(shí)調(diào)用窗口過程。這樣就解決了select中的時(shí)機(jī)問題和里面兩層循環(huán)的問題
WSAAsyncSelect函數(shù)原型如下:
int WSAAsyncSelect(
__in SOCKET s,
__in HWND hWnd,
__in unsigned int wMsg,
__in long lEvent
);
第一個(gè)參數(shù)是綁定的socket,第二個(gè)參數(shù)是消息所對(duì)應(yīng)的窗口句柄,第三個(gè)參數(shù)是對(duì)應(yīng)的消息,這個(gè)消息需要自己定義,第4個(gè)參數(shù)是我們所關(guān)心的事件,當(dāng)在s這個(gè)socket發(fā)生lEvent這個(gè)事件發(fā)生時(shí)會(huì)向hWnd對(duì)應(yīng)的窗口發(fā)送wMsg消息。
在消息附帶的兩個(gè)參數(shù)wParam和lParam中,lParam的高位16位表示當(dāng)前的錯(cuò)誤碼,低16位表示當(dāng)前socket上發(fā)生的事件。其中事件的取值如下:
- FD_WRITE : 當(dāng)socket上可寫時(shí)觸發(fā)該事件,F(xiàn)D_WRITE的觸發(fā)與調(diào)用send沒有必然的聯(lián)系,F(xiàn)D_WRITE只是表示socket已經(jīng)為發(fā)送準(zhǔn)備好了必要的條件,其實(shí)調(diào)用時(shí)可以不必理會(huì)這個(gè)事件,只需要在想發(fā)送數(shù)據(jù)的場(chǎng)合調(diào)用send,一般來說FD_WRITE只在這些條件下觸發(fā):a) 調(diào)用connect函數(shù)成功連接到服務(wù)器 b) 調(diào)用accept接受連接成功后(該條件是綁定在accept返回的那個(gè)與客戶端通訊的socket上) c)調(diào)用send,sendto 失敗并返回WSAWOULDBLOCK(由于是異步操作,可能同時(shí)客戶端也在發(fā)數(shù)據(jù), 此時(shí)可能導(dǎo)致send失敗)
為了方便我們處理這些參數(shù),WinSock 提供了兩個(gè)宏來解析它的高16位和低16位,分別是WSAGETSELECTERROR和WSAGETSELECTEVENT
而lParam則保存了當(dāng)前觸發(fā)事件的socket句柄
如果對(duì)一個(gè)句柄調(diào)用了WSAAsyncSelect 并成功后,對(duì)應(yīng)的socket會(huì)自動(dòng)編程非阻塞模式。它就不像前面的select模型那樣需要顯示調(diào)用ioctrlsocket將socekt設(shè)置為非阻塞。
另外不需要每個(gè)socket都定義一個(gè)消息ID,通常一個(gè)ID已經(jīng)足夠處理所有的socket事件。
下面是一個(gè)具體的例子
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失敗,錯(cuò)誤碼為:%08x\n", WSAGetLastError());
goto __CLEAR_UP;
}
if (0 != bind(skServer, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR)))
{
printf("綁定失敗,錯(cuò)誤碼為:%08x\n", WSAGetLastError());
goto __CLEAR_UP;
}
if (0 != listen(skServer, 5))
{
printf("監(jiān)聽失敗,錯(cuò)誤碼為:%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("有客戶端連接進(jìn)來[%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;
}
<hr />
在上面的代碼中我們?cè)趍ain函數(shù)中創(chuàng)建了窗口程序,而常規(guī)的都是在WinMain中創(chuàng)建,其實(shí)從本質(zhì)上講控制臺(tái)程序和窗口程序都是一個(gè)進(jìn)程,至于以main作為入口還是以WinMain作為入口只是習(xí)慣上這樣,但是并沒有硬性規(guī)定。
在創(chuàng)建窗口之后我們將監(jiān)聽socket也綁定到窗口消息中,然后在對(duì)應(yīng)的消息中判斷FD_ACCEPT事件,如果是則調(diào)用accept進(jìn)行連接。并將對(duì)生成的socket進(jìn)行綁定。
在接下來的socket消息中主要處理FD_READ事件,當(dāng)發(fā)生READ事件時(shí)調(diào)用read接收數(shù)據(jù),然后調(diào)用send將數(shù)據(jù)原封不動(dòng)的發(fā)送出去。
從上面的代碼上看,該模型相對(duì)于select來說省去了查看socket是否在對(duì)應(yīng)數(shù)組中的操作,減少了循環(huán)。而且可以很好的把握什么調(diào)用時(shí)機(jī)問題。
主要的缺點(diǎn)是它需要一個(gè)窗口,這樣在服務(wù)程序中基本就排除掉了這個(gè)模型,它基本上只會(huì)出現(xiàn)在客戶端程序中。
另外如果在一個(gè)窗口中需要管理成千上萬個(gè)句柄時(shí),它的性能會(huì)急劇下降,因此它的伸縮性較差。但是在客戶端中基本不存在這個(gè)問題,所以如果要在客戶端中想要減少編程難度,它是一個(gè)不二的選擇