目錄
引言
一、WSAAsyncSelect模型概述
二、WSAAsyncSelect模型流程
2.1 自定義消息
2.2 創建窗口例程
2.3 初始化套接字
2.4 注冊網絡事件
2.5 綁定和監聽
2.6 消息循環
三、完整示例代碼
引言
????????在網絡編程的廣袤天地中,高效處理網絡事件是構建穩定應用的關鍵。WSAAsyncSelect模型作為一種獨特且實用的網絡編程模型,為開發者提供了異步處理網絡事件的有力手段。它巧妙地將Windows窗口消息機制與套接字相結合,讓應用程序能夠基于消息通知,及時響應各類網絡事件。接下來,讓我們深入探究WSAAsyncSelect模型的工作原理、具體流程以及在實際編程中的應用,一同解鎖其在網絡編程領域的強大潛力。
一、WSAAsyncSelect模型概述
????????Windows 套接字異步選擇模型,要是想在應用程序里用上WSAAsyncSelect模型,第一步就是用CreateWindow函數創建一個窗口,緊接著得給這個窗口配備一個窗口回調函數(WinProc)。除了創建窗口,使用對話框也是可行的,這種情況下就得給對話框配上對話框回調函數。
????????WinSock給出了一個特別好用的異步I/O模型。依靠這個模型,應用程序可以在某個套接字上,接收那些基于Windows消息的網絡事件通知。
????????WSAAsyncSelect模型的實現辦法是這樣的:調用WSASyncSelect函數,這么做會自動把套接字切換到非阻塞模式,與此同時,還能注冊一個或者多個你關心的網絡事件。它會把套接字、窗口句柄以及自定義消息捆綁到一塊兒。只要之前注冊的網絡事件發生了,對應的窗口就會收到一個基于消息的通知 。
二、WSAAsyncSelect模型流程
2.1 自定義消息
????????用戶需要自定義一個消息。當相關網絡事件消息出現時,這個自定義消息會被發送到消息隊列中。一般有以下兩種自定義消息的方式:
1. 靜態注冊消息:
#define WM_MYSOCKETMSG WM_USER + 100; //具體查看自定義消息的范圍
2. 動態注冊消息:
#define MYWN_SOCKET L"MYWN_SOCK" //自定義一個字符串
UINT g_nNetMsgID = RegisterWindowMessage(MYWN_SOCKET);
2.2 創建窗口例程
????????利用WSAAsyncSelect()函數開發WinSock應用程序,離不開Windows窗口。在窗口實例中接收用戶自定義的消息。以Win32應用程序為例:
HWND g_SockHwnd = NULL; //接收SOCKET消息的窗口句柄
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {HWND hWnd;hInst = hInstance; //將實例句柄存儲在全局變量中hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);g_SockHwnd = hWnd;if (!hWnd)return FALSE;ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);return TRUE;
}
2.3 初始化套接字
????????初始化組件和創建套接字的方法之前已經介紹過。
#include"winsock2.h"
#pragma comment(lib,"WS2_32.lib")
WSADATA stcData;
int nResult = 0;
nResult = WSAStartup(MAKEWORD(2, 2), &stcData);
//2.創建套接字
SOCKET sSocket = socket(AF_INET, SOCK_STREAM, 0);
2.4 注冊網絡事件
????????WSAAsyncSelect函數有兩個主要作用,一是它能自動把套接字設置成非阻塞模式,二是會給套接字關聯上一個窗口句柄。一旦有網絡事件出現,比如連接建立、數據發送或接收等情況發生,WSAAsyncSelect函數就會把相關信息發送到之前綁定的那個窗口。這樣,應用程序在接收到像是連接、發送、接收這類網絡通知時,對應的具體信息就會被投放到窗口消息隊列當中 。
int WSAAsyncSelect(SOCKET s, //套接字句柄HWND hWnd, //要響應事件的窗口句柄unsigned int wMsg, //自定義的消息long lEvent //注冊的網絡事件
);
?????????注:其中窗口句柄是在創建主窗口時獲得的。注冊網絡事件通常在創建時設定連接通知和關閉通知。
(1)FD_READ事件觸發條件:
??? - 在數據到達socket后,并且前一個recv()調用完畢。
??? - 調用recv()后,緩沖區還有未讀完的數據時,還會繼續響應該事件。
(2)FD_WRITE事件觸發條件:
??? - 第一次connect()或accept()后(即連接建立后)。
??? - 調用send()返回WSAEWOULDBLOCK錯誤后,再次調用send()或sendto函數成功時。
(3)FD_ACCEPT事件觸發條件:當有請求建立連接,并且前一個accept()調用后。
(4)FD_CLOSE事件觸發條件:自己或客戶端中斷連接后。
(5)FD_CONNECT事件觸發條件:調用了connect(),并且連接建立后。
示例:
WSAAsyncSelect(sSocket,g_SockHwnd, g_nNetMsgID, //當前服務端的SOCK句柄//當前服務端的窗口句柄//當有網絡事件響應時窗口接收的消息FD_ACCEPT | FD_CLOSE);//需要響應的網絡事件消息
2.5 綁定和監聽
1. 初始化地址定址:
sockaddr_in sAddr = {0};
sAddr.sin_family = AF_INET;
sAddr.sin_port = htons(1234);
sAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
2. 綁定:
int nRet = 0;
nRet = bind(sSocket,(sockaddr*)&sAddr,sizeof(sockaddr_in));
//接收返回信息
//當前客戶端SOCK句柄
//IP定址
//IP定址結構體大小
3. 監聽:
nRet = listen(sSocket, SOMAXCONN);
//當前服務端的SOCK句柄
//等待連接的最大隊列長度
2.6 消息循環
在消息循環中實現自定義消息的處理過程。
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
當網絡事件消息抵達一個窗口回調函數后:
- `message`為當前自定義消息。
- `wParam`為當前響應網絡事件的套接字。
- `lParam`的高16位為錯誤碼,低16位為具體的網絡事件。
通過以下宏來獲取相關信息:
- `WSAGETSELECTERROR`:返回`lParam`高字位包含的錯誤信息。
- `WSAGETSELECTEVENT`:根據得到的`lParam`的低字部分確定具體是哪一類網絡事件。
示例代碼:
int lError = WSAGETSELECTERROR(lParam); //高16位表示錯誤碼
int lEvent = WSAGETSELECTEVENT(lParam); //低字節為發生的網絡事件
SOCKET MsgSocket = (SOCKET)wParam; //消息事件
switch (lEvent) {
case FD_ACCEPT:sockaddr_in ClientAddr = {};int nClientLength = sizeof(ClientAddr);SOCKET sClientSock = accept(MsgSocket,//客戶端地址信息//客戶端地址信息長度//當前服務端的SOCK句柄(sockaddr*)&ClientAddr, &nClientLength);//設置消息模式WSAAsyncSelect(sClientSock, g_SockHwnd, g_nNetMsgID,FD_READ | FD_WRITE | FD_CLOSE);
}
接收不到網絡事件的原因
????????1. 在同一個套接字上,自定義的網絡事件窗口消息被多次調用WSAAsyncSelect()函數注冊不同的網絡事件,這種情況下以最后一次注冊的網絡事件為準。例如:
WSAAsyncSelect(s, hWnd, wm_msg, FD_READ);
WSAAsyncSelect(s, hWnd, wm_msg, FD_ACCEPT);
????????2. 在同一個套接字上多次調用WSAAsyncSelect()函數,且使用了不同的網絡事件窗口消息。例如:
WSAAsyncSelect(s, hWnd, wm_msg1, FD_READ);
WSAAsyncSelect(s, hWnd, wm_msg2, FD_READ);
以下是示例代碼部分:
#include "WSAAsyncSelect.h"
UINT g_nNetMsgID = 0;
HWND g_SockHwnd = NULL;
SOCKET g_sClientSock = NULL;
//套接字消息
//接收SOCKET消息的窗口句柄
//客戶端Socket句柄
#define WM_MYSOCKETMSG WM_USER + 100;
BOOL AsyncSelectTCP() {//1.初始化套接字WSADATA stcData;int nResult = 0;nResult = WSAStartup(MAKEWORD(2, 2), &stcData);if (nResult == SOCKET_ERROR)return FALSE;g_nNetMsgID = RegisterWindowMessage(MYWN_SOCKET);//2.創建套接字SOCKET sSocket = Socket(AF_INET, SOCK_STREAM, 0);//3.注冊感興趣的網絡事件//注冊消息//當前服務端的SOCK句柄int nRet = WSAAsyncSelect(sSocket, g_SockHwnd,//當前服務端的窗口句柄g_nNetMsgID,//當有網絡事件響應時窗口接收的消息FD_ACCEPT | FD_CLOSE);//需要響應的網絡事件消息if (nRet) {MessageBox(NULL, L"", L"在監聽SOCKET上設置網絡消息失敗", MB_OK);goto CloseSock;}//4.初始化地址定址sockaddr_in sAddr = {0};sAddr.sin_family = AF_INET;sAddr.sin_port = htons(1234);sAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//5.綁定nRet = bind(sSocket,(sockaddr*)&sAddr,sizeof(sockaddr_in));//當前客戶端SOCK句柄//IP定址//IP定址結構體大小if (SOCKET_ERROR == nRet) {MessageBox(NULL, L"", L"綁定到指定地址端口出錯!", MB_OK);goto CloseSock;}//6.監聽 在調用WSAAsyncSelect后sSocket已經是非阻塞模式nRet = listen(sSocket, //當前服務端的SOCK句柄SOMAXCONN); //等待連接的最大隊列長度if (SOCKET_ERROR == nRet) {MessageBox(NULL, L"錯誤", L"SOCKET進入監聽模式出錯!", MB_OK);goto CloseSock;}return TRUE;
CloseSock:closesocket(sSocket);WSACleanup();return FALSE;
}
//************************************
//函數名稱: SocketMsg響應網絡事件消息
//返回值:
//參數:
//參數:
//參數:
//參數:
//************************************
void SocketMsg(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {int iError = WSAGETSELECTERROR(lParam);int lEvent = WSAGETSELECTEVENT(lParam);SOCKET MsgSocket = (SOCKET)wParam;switch (lEvent) {//高16位表示錯誤碼//低字節為發生的網絡事件//響應消息事件的套接字case FD_ACCEPT: {sockaddr_in ClientAddr = {};int nClientLength = sizeof(ClientAddr);//客戶端地址信息長度if (!g_sClientSock) //當前例子只允許連接一個客戶端{g_sClientSock = accept(MsgSocket, //當前服務端的SOCK句柄(sockaddr*)&ClientAddr,&nClientLength);//重新為該消息設置網絡事件WSAAsyncSelect(sClientSock, g_SockHwnd, g_nNetMsgID,FD_READ | FD_WRITE | FD_CLOSE);}break;}case FD_CLOSE: {closesocket(g_sClientSock);}break;case FD_READ:char szBufTmp[1024] = {0};if (g_sClientSock) {int iRecv = recv(MsgSocket, szBufTmp, 1024, 0);if (SOCKET_ERROR == iRecv || 0 == iRecv) {if (WSAEWOULDBLOCK == WSAGetLastError())Sleep(20);//停20ms}else {//顯示信息}}break;}
}
在`XXX主窗口.cpp`文件中:
????????1. 在`InitInstance`函數中創建窗口時,保存該窗口句柄:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {HWND hWnd;hInst = hInstance; //將實例句柄存儲在全局變量中hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);g_SockHwnd = hWnd;if (!hWnd) {return FALSE;}ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);return TRUE;
}
????????2. 在窗口回調函數中判斷當前消息是否為自定義消息:
if (g_nNetMsgID == message) {SocketMsg(hWnd, message, wParam, lParam);
}
????????WSAAsyncSelect模型通過將套接字與窗口消息機制相結合,為網絡編程提供了一種異步處理網絡事件的方式,在實際應用中有助于提升程序對網絡事件的響應效率和處理能力 。
?
三、完整示例代碼
TCP客戶端代碼:
待補充
- 這是一個基礎的TCP客戶端實現
- 主要功能:
? - 連接到服務器(127.0.0.1:0x1234)
? - 使用多線程處理接收消息
? - 通過控制臺輸入發送消息
? - 基本的錯誤處理?
WSAAsyncSelect模型服務端:
待補充
?這是一個基于Windows消息機制的TCP服務器實現
- 主要特點:
? - 使用WSAAsyncSelect實現異步通信
? - 通過Windows消息機制處理網絡事件
? - 支持多客戶端連接
? - 使用自定義消息(WM_MYSOCKET)處理網絡事件
? - 在界面上顯示連接狀態和消息
?
?