目录
[2.1 自定义消息](#2.1 自定义消息)
[2.2 创建窗口例程](#2.2 创建窗口例程)
[2.3 初始化套接字](#2.3 初始化套接字)
[2.4 注册网络事件](#2.4 注册网络事件)
[2.5 绑定和监听](#2.5 绑定和监听)
[2.6 消息循环](#2.6 消息循环)
引言
在网络编程的广袤天地中,高效处理网络事件是构建稳定应用的关键。WSAAsyncSelect模型作为一种独特且实用的网络编程模型,为开发者提供了异步处理网络事件的有力手段。它巧妙地将Windows窗口消息机制与套接字相结合,让应用程序能够基于消息通知,及时响应各类网络事件。接下来,让我们深入探究WSAAsyncSelect模型的工作原理、具体流程以及在实际编程中的应用,一同解锁其在网络编程领域的强大潜力。
一、WSAAsyncSelect模型概述
Windows 套接字异步选择模型,要是想在应用程序里用上WSAAsyncSelect模型,第一步就是用CreateWindow函数创建一个窗口,紧接着得给这个窗口配备一个窗口回调函数(WinProc)。除了创建窗口,使用对话框也是可行的,这种情况下就得给对话框配上对话框回调函数。
WinSock给出了一个特别好用的异步I/O模型。依靠这个模型,应用程序可以在某个套接字上,接收那些基于Windows消息的网络事件通知。
WSAAsyncSelect模型的实现办法是这样的:调用WSASyncSelect函数,这么做会自动把套接字切换到非阻塞模式,与此同时,还能注册一个或者多个你关心的网络事件。它会把套接字、窗口句柄以及自定义消息捆绑到一块儿。只要之前注册的网络事件发生了,对应的窗口就会收到一个基于消息的通知 。
二、WSAAsyncSelect模型流程
2.1 自定义消息
用户需要自定义一个消息。当相关网络事件消息出现时,这个自定义消息会被发送到消息队列中。一般有以下两种自定义消息的方式:
- 静态注册消息:
cpp
#define WM_MYSOCKETMSG WM_USER + 100; //具体查看自定义消息的范围
2. 动态注册消息:
cpp
#define MYWN_SOCKET L"MYWN_SOCK" //自定义一个字符串
UINT g_nNetMsgID = RegisterWindowMessage(MYWN_SOCKET);
2.2 创建窗口例程
利用WSAAsyncSelect()函数开发WinSock应用程序,离不开Windows窗口。在窗口实例中接收用户自定义的消息。以Win32应用程序为例:
cpp
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 初始化套接字
初始化组件和创建套接字的方法之前已经介绍过。
cpp
#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函数就会把相关信息发送到之前绑定的那个窗口。这样,应用程序在接收到像是连接、发送、接收这类网络通知时,对应的具体信息就会被投放到窗口消息队列当中 。
cpp
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(),并且连接建立后。
示例:
cpp
WSAAsyncSelect(sSocket,
g_SockHwnd,
g_nNetMsgID,
//当前服务端的SOCK句柄
//当前服务端的窗口句柄
//当有网络事件响应时窗口接收的消息
FD_ACCEPT | FD_CLOSE);
//需要响应的网络事件消息
2.5 绑定和监听
- 初始化地址定址:
cpp
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");
- 绑定:
cpp
int nRet = 0;
nRet = bind(sSocket,
(sockaddr*)&sAddr,
sizeof(sockaddr_in));
//接收返回信息
//当前客户端SOCK句柄
//IP定址
//IP定址结构体大小
- 监听:
cpp
nRet = listen(sSocket, SOMAXCONN);
//当前服务端的SOCK句柄
//等待连接的最大队列长度
2.6 消息循环
在消息循环中实现自定义消息的处理过程。
cpp
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
当网络事件消息抵达一个窗口回调函数后:
-
`message`为当前自定义消息。
-
`wParam`为当前响应网络事件的套接字。
-
`lParam`的高16位为错误码,低16位为具体的网络事件。
通过以下宏来获取相关信息:
-
`WSAGETSELECTERROR`:返回`lParam`高字位包含的错误信息。
-
`WSAGETSELECTEVENT`:根据得到的`lParam`的低字部分确定具体是哪一类网络事件。
示例代码:
cpp
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);
}
接收不到网络事件的原因
- 在同一个套接字上,自定义的网络事件窗口消息被多次调用WSAAsyncSelect()函数注册不同的网络事件,这种情况下以最后一次注册的网络事件为准。例如:
cpp
WSAAsyncSelect(s, hWnd, wm_msg, FD_READ);
WSAAsyncSelect(s, hWnd, wm_msg, FD_ACCEPT);
- 在同一个套接字上多次调用WSAAsyncSelect()函数,且使用了不同的网络事件窗口消息。例如:
cpp
WSAAsyncSelect(s, hWnd, wm_msg1, FD_READ);
WSAAsyncSelect(s, hWnd, wm_msg2, FD_READ);
以下是示例代码部分:
cpp
#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`文件中:
- 在`InitInstance`函数中创建窗口时,保存该窗口句柄:
cpp
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;
}
- 在窗口回调函数中判断当前消息是否为自定义消息:
cpp
if (g_nNetMsgID == message) {
SocketMsg(hWnd, message, wParam, lParam);
}
WSAAsyncSelect模型通过将套接字与窗口消息机制相结合,为网络编程提供了一种异步处理网络事件的方式,在实际应用中有助于提升程序对网络事件的响应效率和处理能力 。
三、完整示例代码
TCP客户端代码:
待补充
- 这是一个基础的TCP客户端实现
-
主要功能:
-
连接到服务器(127.0.0.1:0x1234)
-
使用多线程处理接收消息
-
通过控制台输入发送消息
-
基本的错误处理
WSAAsyncSelect模型服务端:
待补充
这是一个基于Windows消息机制的TCP服务器实现
-
主要特点:
-
使用WSAAsyncSelect实现异步通信
-
通过Windows消息机制处理网络事件
-
支持多客户端连接
-
使用自定义消息(WM_MYSOCKET)处理网络事件
-
在界面上显示连接状态和消息