WinSock I/O 模型 -- WSAAsyncSelect 模型
簡介
WSAAsyncSelect 模型也是 WinSock 中常見的異步 I/O 模型。
使用這個(gè)模型,網(wǎng)絡(luò)應(yīng)用程序通過接收以 Windows 消息為基礎(chǔ)的網(wǎng)絡(luò)事件通知來處理網(wǎng)絡(luò)請(qǐng)求。
這篇文章我們就來看看如何使用 WSAAsyncSelect api 來實(shí)現(xiàn)一個(gè)簡單的 TCP 服務(wù)器.
API 基礎(chǔ)
要使用 WSAAsyncSelect 模型,我們必須創(chuàng)建一個(gè)窗口, 再為該窗口對(duì)象提供一個(gè)窗口歷程(WinProc). 通過適當(dāng)?shù)呐渲弥?#xff0c;當(dāng)有網(wǎng)絡(luò)請(qǐng)求到來的時(shí)候,windows 會(huì)將網(wǎng)絡(luò)消息投遞到我們所創(chuàng)建的窗口對(duì)象上,然后我們通過對(duì)應(yīng)的窗口例程來處理該請(qǐng)求.
WinProc
WindowProc 回調(diào)函數(shù)用來處理 Windows 系統(tǒng)投遞到特定窗口的消息。
它的方法簽名如下:
LRESULT CALLBACK WindowProc(_In_ HWND hwnd,_In_ UINT uMsg,_In_ WPARAM wParam,_In_ LPARAM lParam );- hwnd:當(dāng)前窗口消息關(guān)聯(lián)的窗口句柄
- uMsg:消息值
- wParam: 額外的消息信息。 具體含義依賴于 uMsg 的值
- lParam:額外的消息信息。 具體含義依賴于 uMsg 的值
RegisterClass
RegisterClass 用來注冊(cè)一個(gè)窗口類型,以便在后續(xù)的 CreateWindow 或 CreateWindowEx 中使用.
ATOM RegisterClassA(const WNDCLASSA *lpWndClass );這里不詳細(xì)介紹該函數(shù)的用法,參考 實(shí)例 章節(jié)。
值得注意的是,我們的窗口例程(WinProc 函數(shù))便需要設(shè)置到 lpWndClass 對(duì)象上. 同時(shí)非常重要的是,這個(gè)類型上還需要包含我們需要注冊(cè)的窗口類型的名稱.
CreateWindowEx
CreateWindowEx 用來創(chuàng)建一個(gè)窗口對(duì)象.
HWND CreateWindowExA(DWORD dwExStyle,LPCSTR lpClassName,LPCSTR lpWindowName,DWORD dwStyle,int X,int Y,int nWidth,int nHeight,HWND hWndParent,HMENU hMenu,HINSTANCE hInstance,LPVOID lpParam );在我們的程序中,我們僅僅需要一個(gè)簡單的窗口對(duì)象,因此在我們的實(shí)例中絕大部分參數(shù)都是用默認(rèn)值。 這里也不詳細(xì)展開,用法參考 實(shí)例 章節(jié)。
WSAAsyncSelect
WSAAsyncSelect 用于將窗口和 SOCKET 對(duì)象綁定起來,可以指定關(guān)心的 SOCKET 事件.
int WSAAsyncSelect(SOCKET s,HWND hWnd,u_int wMsg,long lEvent );wMsg 指定一個(gè)消息值,當(dāng)對(duì)應(yīng)的 SOCKET 上有 SOCKET 事件發(fā)生的時(shí)候,窗口例程會(huì)被調(diào)用,這個(gè)消息值會(huì)被傳回來給我們。主要用于區(qū)別系統(tǒng)的窗口事件和我們自定義的事件.
GetMessage
GetMessage 從當(dāng)前線程的消息隊(duì)列中獲取消息。
BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax );- lpMsg: 是一個(gè) MSG 結(jié)構(gòu)體,用來接收消息信息
- hWnd:指定想要獲取窗口信息的窗口句柄
- wMsgFilterMin:略
- wMsgFilterMax:略
TranslateMessage
TranslateMessage 用于將 virtual-key message 轉(zhuǎn)化為 character message. character mesage。 character message 可以再使用 DispatchMessage 將消息分發(fā)到窗口例程(WinProc 函數(shù))。
BOOL TranslateMessage(const MSG *lpMsg );DispatchMessage
DispatchMessage 用于將窗口消息分發(fā)到窗口例程(WinProc 函數(shù))。
LRESULT DispatchMessage(const MSG *lpMsg );實(shí)現(xiàn)思路
實(shí)例
這里我們通過一個(gè)實(shí)例來看看如何實(shí)現(xiàn):
#define WIN32_LEAN_AND_MEAN #define _WINSOCK_DEPRECATED_NO_WARNINGS#include <winsock2.h> #include <windows.h> #include <stdio.h> #include <conio.h>#pragma comment(lib,"ws2_32.lib")#define PORT 8080 #define DATA_BUFSIZE 8192typedef struct _SOCKET_CONTEXT {BOOL RecvPosted;CHAR Buffer[DATA_BUFSIZE];WSABUF DataBuf;SOCKET Socket;DWORD BytesSEND;DWORD BytesRECV;struct _SOCKET_CONTEXT *Next; } SOCKET_CONTEXT, *LPSOCKET_CONTEXT;// 我們使用 WM_SOCKET 作為 SOCKET 消息的消息值, 這樣在 WinProc 中我們可以通過檢查 // 當(dāng)前消息的消息值是否是 VM_SOCKET來決定是否處理該消息 #define WM_SOCKET (WM_USER + 1)void CreateSocketContext(SOCKET s); LPSOCKET_CONTEXT GetSocketContext(SOCKET s); void FreeSocketContext(SOCKET s); HWND MakeWorkerWindow(void); LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LPSOCKET_CONTEXT SocketContexts;int main() {MSG msg;DWORD Ret;SOCKET ListenSocket;SOCKADDR_IN Addr;HWND Window;WSADATA wsaData;// 創(chuàng)建用戶接收 SOCKET 事件消息的窗口,將 WinProc 函數(shù)指定為 WindowProcif ((Window = MakeWorkerWindow()) == NULL) {printf("MakeWorkerWindow() failed!\n");return 1;}// 初始化 Listen Socket 對(duì)象if (WSAStartup(0x0202, &wsaData) != 0) {printf("WSAStartup() failed with error %d\n", WSAGetLastError());return 1;}if ((ListenSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {printf("socket() failed with error %d\n", WSAGetLastError());return 1;}// 將 ListenSocket 與我們創(chuàng)建的 Window 關(guān)聯(lián)起來// 指定 SOCKET 消息的消息值為 WM_SOCKET// 我們關(guān)心的 ListenSocket 事件為: FD_ACCEPT 和 FD_CLOSEif(WSAAsyncSelect(ListenSocket, Window, WM_SOCKET, FD_ACCEPT|FD_CLOSE) != 0) {printf("WSAAsyncSelect() failed with error code %d\n", WSAGetLastError());return 1;}Addr.sin_family = AF_INET;Addr.sin_addr.s_addr = htonl(INADDR_ANY);Addr.sin_port = htons(PORT);if (bind(ListenSocket, (PSOCKADDR) &Addr, sizeof(Addr)) == SOCKET_ERROR) {printf("bind() failed with error %d\n", WSAGetLastError());return 1;}if (listen(ListenSocket, 10)) {printf("listen() failed with error %d\n", WSAGetLastError());return 1;}// 循環(huán),處理當(dāng)前消息隊(duì)列中的消息// 當(dāng)有新的消息可用時(shí),使用 TranslateMessage 轉(zhuǎn)化消息,并將轉(zhuǎn)換后的消息通過 DispatchMessage 分發(fā)到我們的 WinProc 函數(shù) while(Ret = GetMessage(&msg, NULL, 0, 0)) {if (Ret == -1) {printf("\nGetMessage() failed with error %d\n", GetLastError());return 1;}TranslateMessage(&msg);printf("Dispatching a message...\n");DispatchMessage(&msg);} }// 當(dāng)有新的消息可用時(shí),windows 操作系統(tǒng)會(huì)回調(diào)我們這個(gè)函數(shù) LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {SOCKET AcceptSocket;LPSOCKET_CONTEXT SocketContext;DWORD RecvBytes;DWORD SendBytes;DWORD Flags;// 我們僅僅關(guān)系消息值是 WM_SOCKET 的消息,其他消息值代表系統(tǒng)的消息,我們不處理// 對(duì)于其他消息,我們使用 DefWindowProc 函數(shù)來調(diào)用系統(tǒng)默認(rèn)窗口例程來處理該消息if (uMsg == WM_SOCKET) {// 使用 WSAGETSELECTERROR 來檢查是否發(fā)生了 SOCKET 錯(cuò)誤if (WSAGETSELECTERROR(lParam)) {printf("Socket failed with error %d\n", WSAGETSELECTERROR(lParam));FreeSocketContext(wParam);} else {// 使用 WSAGETSELECTEVENT 來獲取具體的 SOCKET 消息類型switch(WSAGETSELECTEVENT(lParam)) {// 有新的客戶端連接請(qǐng)求,接收它case FD_ACCEPT:if ((AcceptSocket = accept(wParam, NULL, NULL)) == INVALID_SOCKET) {printf("accept() failed with error %d\n", WSAGetLastError());break;}CreateSocketContext(AcceptSocket);printf("Socket number %d connected\n", AcceptSocket);// 將新的 SOCKET 與我們的窗口句柄關(guān)聯(lián),這樣我們便能獲取到這個(gè) SOCKET 上的所有消息了。 // 對(duì)于客戶端連接,我們關(guān)心的事件類型包括: FD_READ, FD_WRITE, FD_CLOSEWSAAsyncSelect(AcceptSocket, hwnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);break;// 客戶端鏈接上有數(shù)據(jù)到來,讀取數(shù)據(jù)case FD_READ:SocketContext = GetSocketContext(wParam);if (SocketContext->BytesRECV != 0) {SocketContext->RecvPosted = TRUE;return 0;} else {SocketContext->DataBuf.buf = SocketContext->Buffer;SocketContext->DataBuf.len = DATA_BUFSIZE;Flags = 0;if (WSARecv(SocketContext->Socket, &(SocketContext->DataBuf), 1, &RecvBytes, &Flags, NULL, NULL) == SOCKET_ERROR) {if (WSAGetLastError() != WSAEWOULDBLOCK) {printf("WSARecv() failed with error %d\n", WSAGetLastError());FreeSocketContext(wParam);return 0;}} else {printf("WSARecv() is OK!\n");SocketContext->BytesRECV = RecvBytes;}}// 客戶端可以寫入數(shù)據(jù)(之前的數(shù)據(jù)已經(jīng)全部發(fā)送,或者連接剛剛建立)case FD_WRITE:SocketContext = GetSocketContext(wParam);if (SocketContext->BytesRECV > SocketContext->BytesSEND) {SocketContext->DataBuf.buf = SocketContext->Buffer + SocketContext->BytesSEND;SocketContext->DataBuf.len = SocketContext->BytesRECV - SocketContext->BytesSEND;if (WSASend(SocketContext->Socket, &(SocketContext->DataBuf), 1, &SendBytes, 0, NULL, NULL) == SOCKET_ERROR) {if (WSAGetLastError() != WSAEWOULDBLOCK) {printf("WSASend() failed with error %d\n", WSAGetLastError());FreeSocketContext(wParam);return 0;}} else { // No error so update the byte countprintf("WSASend() is OK!\n");SocketContext->BytesSEND += SendBytes;}}if (SocketContext->BytesSEND == SocketContext->BytesRECV) {SocketContext->BytesSEND = 0;SocketContext->BytesRECV = 0;if (SocketContext->RecvPosted == TRUE) {SocketContext->RecvPosted = FALSE;// 這里我們通過 PostMessage 來發(fā)送 FD_READ 消息PostMessage(hwnd, WM_SOCKET, wParam, FD_READ);}}break;case FD_CLOSE:printf("Closing socket %d\n", wParam);FreeSocketContext(wParam);break;}}return 0;}return DefWindowProc(hwnd, uMsg, wParam, lParam); }// 創(chuàng)建窗口句柄的函數(shù) HWND MakeWorkerWindow(void) {WNDCLASS wndclass;CHAR *ProviderClass = (CHAR*)"AsyncSelect";HWND Window;wndclass.style = CS_HREDRAW | CS_VREDRAW;// 這里非常重要:我們的 WindowProc 注冊(cè)到當(dāng)前 WNDClass 上// 這樣當(dāng)我們將 SOCKET 和我們這個(gè)窗口類型的窗口句柄關(guān)聯(lián)起來后,// 我們便會(huì)在 WindowProc 中接受到對(duì)應(yīng)的 SOCKET 消息wndclass.lpfnWndProc = (WNDPROC)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 = (LPCSTR)ProviderClass;if (RegisterClass(&wndclass) == 0) {printf("RegisterClass() failed with error %d\n", GetLastError());return NULL;}// Create a windowWindow = CreateWindowEx (0, // Optional window styles.(LPCSTR)ProviderClass, // Window class"TEST", // Window textWS_OVERLAPPEDWINDOW, // Window styleCW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL,NULL,NULL);if (Window == NULL) {printf("CreateWindow() failed with error %d\n", GetLastError());return NULL;}return Window; }void CreateSocketContext(SOCKET s) {LPSOCKET_CONTEXT SocketContxt;if ((SocketContxt = (LPSOCKET_CONTEXT) GlobalAlloc(GPTR, sizeof(SOCKET_CONTEXT))) == NULL) {printf("GlobalAlloc() failed with error %d\n", GetLastError());return;}// Prepare SocketInfo structure for useSocketContxt->Socket = s;SocketContxt->RecvPosted = FALSE;SocketContxt->BytesSEND = 0;SocketContxt->BytesRECV = 0;SocketContxt->Next = SocketContexts;SocketContexts = SocketContxt; }LPSOCKET_CONTEXT GetSocketContext(SOCKET s) {SOCKET_CONTEXT *SocketContext = SocketContexts;while(SocketContext) {if (SocketContext->Socket == s) return SocketContext;SocketContext = SocketContext->Next;}return NULL; }void FreeSocketContext(SOCKET s) {SOCKET_CONTEXT *SocketContext = SocketContexts;SOCKET_CONTEXT *PrevSocketContext = NULL;while(SocketContext) {if (SocketContext->Socket == s) {if (PrevSocketContext) PrevSocketContext->Next = SocketContext->Next;else SocketContexts = SocketContext->Next;closesocket(SocketContext->Socket);GlobalFree(SocketContext);return;}PrevSocketContext = SocketContext;SocketContext = SocketContext->Next;} }END ! ! !
總結(jié)
以上是生活随笔為你收集整理的WinSock I/O 模型 -- WSAAsyncSelect 模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于SimpleDateFormat线程
- 下一篇: uCOS-III应用开发笔记之一:uCO