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 变回阻塞模式。
示例代码
测试环境
-
测试环境:
服务端 :vs2019
客户端:linux ubuntu 1804 64位 NC命令模拟客户端
-
服务端创建类型: window应用程序(创建时选择 Windws桌面向导)
我们来看一个具体的示例代码:
// 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 命令模拟几个客户端,模拟命令如下:
//本地电脑ip地址:192.168.43.92
shixun9211@ubuntu:~$ nc -v 192.168.43.92 6000
Connection to 192.168.43.92 6000 port [tcp/x11] succeeded!
hello
world
?
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 这个函数封装的。