Windows 的 WSAAsyncSelect 網絡通信模型
**WSAAsyncSelect ** 是 Windows 系統非常常用一個網絡通信模型,它的原理是將 socket 句柄綁定到一個 Windows 窗口上並利於 Windows 的窗口消息機制實現了網絡有消息時調用窗口函數。**WSAAsyncSelect ** 函數簽名如下:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
u_int wMsg,
long lEvent
);
參數 s 和 hwnd 是需要綁定在一起的 socket 句柄和窗口句柄,參數 uMsg 是自定義的一個窗口消息,socket 有事件時會產生這個消息類型,爲了避免與 Windows 內置消息衝突,通常這個消息值應該在 WM_USER 基礎之上定義(如 WM_USER + 1),參數 lEvent 即 要監聽的 socket 事件類型,它的取值是上一小節介紹的 FD_XXX 系列。函數調用成功返回 0 值,調用失敗返回 SOCKET_ERROR(-1)。
WSAAsyncSelect 如果設置了 lEvent 值(非 0),會自動將參數 s 對應的 socket 設置爲非阻塞模式;反之,如果設置 lEvent = 0 會自動將 socket 變回阻塞模式。
我們來看一個具體的示例代碼:
// WSAAsyncSelect.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include <winsock2.h>
#include "WSAAsyncSelect.h"
#pragma comment(lib, "ws2_32.lib")
//socket 消息
#define WM_SOCKET WM_USER + 1
//當前在線用戶數量
int g_nCount = 0;
SOCKET InitSocket();
ATOM MyRegisterClass(HINSTANCE hInstance);
HWND InitInstance(HINSTANCE hInstance, int nCmdShow);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT OnSocketEvent(HWND hWnd, WPARAM wParam, LPARAM lParam);
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
SOCKET hListenSocket = InitSocket();
if (hListenSocket == INVALID_SOCKET)
return 1;
MSG msg;
MyRegisterClass(hInstance);
HWND hwnd = InitInstance(hInstance, nCmdShow);
if (hwnd == NULL)
return 1;
//利用 WSAAsyncSelect 將偵聽 socket 與 hwnd 綁定在一起
if (WSAAsyncSelect(hListenSocket, hwnd, WM_SOCKET, FD_ACCEPT) == SOCKET_ERROR)
return 1;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
closesocket(hListenSocket);
WSACleanup();
return (int) msg.wParam;
}
SOCKET InitSocket()
{
//1. 初始化套接字庫
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
int nError = WSAStartup(wVersionRequested, &wsaData);
if (nError != 0)
return INVALID_SOCKET;
if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup();
return INVALID_SOCKET;
}
//2. 創建用於監聽的套接字
SOCKET hListenSocket = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
//3. 綁定套接字
if (bind(hListenSocket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
closesocket(hListenSocket);
WSACleanup();
return INVALID_SOCKET;
}
//4. 將套接字設爲監聽模式,準備接受客戶請求
if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR)
{
closesocket(hListenSocket);
WSACleanup();
return INVALID_SOCKET;
}
return hListenSocket;
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WSAASYNCSELECT));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = _T("DemoWindowCls");
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassEx(&wcex);
}
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd = CreateWindow(_T("DemoWindowCls"), _T("DemoWindow"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
return NULL;
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return hWnd;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (uMsg)
{
case WM_SOCKET:
return OnSocketEvent(hWnd, wParam, lParam);
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
LRESULT OnSocketEvent(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
SOCKET s = (SOCKET)wParam;
int nEventType = WSAGETSELECTEVENT(lParam);
int nErrorCode = WSAGETSELECTERROR(lParam);
if (nErrorCode != 0)
return 1;
switch (nEventType)
{
case FD_ACCEPT:
{
//調用accept函數處理接受連接事件;
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
//等待客戶請求到來
SOCKET hSockClient = accept(s, (SOCKADDR*)&addrClient, &len);
if (hSockClient != SOCKET_ERROR)
{
//產生的客戶端socket,監聽其 FD_READ/FD_CLOSE 事件
if (WSAAsyncSelect(hSockClient, hWnd, WM_SOCKET, FD_READ | FD_CLOSE) == SOCKET_ERROR)
{
closesocket(hSockClient);
return 1;
}
g_nCount++;
TCHAR szLogMsg[64];
wsprintf(szLogMsg, _T("a client connected, socket: %d, current: %d\n"), (int)hSockClient, g_nCount);
OutputDebugString(szLogMsg);
}
}
break;
case FD_READ:
{
char szBuf[64] = { 0 };
int n = recv(s, szBuf, 64, 0);
if (n > 0)
{
OutputDebugStringA(szBuf);
}
else if (n <= 0)
{
g_nCount--;
TCHAR szLogMsg[64];
wsprintf(szLogMsg, _T("a client disconnected, socket: %d, current: %d\n"), (int)s, g_nCount);
OutputDebugString(szLogMsg);
closesocket(s);
}
}
break;
case FD_CLOSE:
{
g_nCount--;
TCHAR szLogMsg[64];
wsprintf(szLogMsg, _T("a client disconnected, socket: %d, current: %d\n"), (int)s, g_nCount);
OutputDebugString(szLogMsg);
closesocket(s);
}
break;
}// end switch
return 0;
}
在 Visual Studio 中編譯該程序,然後在另外一臺 Linux 機器上使用 nc 命令模擬幾個客戶端,模擬命令如下:
# 我的服務器地址是 192.168.1.131
[root@localhost ~]# nc -v 192.168.1.131 6000
Windows 服務程序的輸出是使用 OutputDebugString 函數來輸出到 Visual Studio 的 Output 窗口中去的,所以需要在調試模式下運行服務程序。Output 窗口 輸出效果如下:
上述代碼中有幾個地方需要注意:
-
當產生了 WM_SOCKET 消息時,消息攜帶的參數 wParam 的值是產生網絡事件的 socket 句柄值,參數 lParam 分爲兩段,高 16 位(bit)(2 字節)是網絡錯誤碼(0 爲沒有錯位),低 16 位(bit)(2 字節)是網絡事件類型,Windows 專門爲了取得這兩個值分別定義了宏 WSAGETSELECTERROR 和 WSAGETSELECTEVENT。
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam) #define WSAGETSELECTERROR(lParam) HIWORD(lParam)
-
對於偵聽 socket, 我們這裏只關注其 FD_CONNECT 事件,對於普通 socket 我們關注其 FD_READ 和 FD_CLOSE 事件。
mfc 中的 CAsyncSocket 類的實現就是基於 WSAAsyncSelect 這個函數封裝的。
本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。
歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391 。