網絡通信基礎重難點解析 14 :Windows 的 WSAAsyncSelect 網絡通信模型

Windows 的 WSAAsyncSelect 網絡通信模型

**WSAAsyncSelect ** 是 Windows 系統非常常用一個網絡通信模型,它的原理是將 socket 句柄綁定到一個 Windows 窗口上並利於 Windows 的窗口消息機制實現了網絡有消息時調用窗口函數。**WSAAsyncSelect ** 函數簽名如下:

int WSAAsyncSelect(
  SOCKET s,
  HWND   hWnd,
  u_int  wMsg,
  long   lEvent
);

參數 shwnd 是需要綁定在一起的 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 專門爲了取得這兩個值分別定義了宏 WSAGETSELECTERRORWSAGETSELECTEVENT

    #define WSAGETSELECTEVENT(lParam)       LOWORD(lParam)
    #define WSAGETSELECTERROR(lParam)       HIWORD(lParam)
    
  • 對於偵聽 socket, 我們這裏只關注其 FD_CONNECT 事件,對於普通 socket 我們關注其 FD_READ 和 FD_CLOSE 事件。

mfc 中的 CAsyncSocket 類的實現就是基於 WSAAsyncSelect 這個函數封裝的。


本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391
微信掃碼關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章