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