【第22节】windows网络编程模型(WSAAsyncSelect模型)

目录

引言

一、WSAAsyncSelect模型概述

二、WSAAsyncSelect模型流程

[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 自定义消息

用户需要自定义一个消息。当相关网络事件消息出现时,这个自定义消息会被发送到消息队列中。一般有以下两种自定义消息的方式:

  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 绑定和监听

  1. 初始化地址定址:
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");
  1. 绑定:
cpp 复制代码
int nRet = 0;
nRet = bind(sSocket,
    (sockaddr*)&sAddr,
    sizeof(sockaddr_in));
//接收返回信息
//当前客户端SOCK句柄
//IP定址
//IP定址结构体大小
  1. 监听:
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);
}

接收不到网络事件的原因

  1. 在同一个套接字上,自定义的网络事件窗口消息被多次调用WSAAsyncSelect()函数注册不同的网络事件,这种情况下以最后一次注册的网络事件为准。例如:
cpp 复制代码
WSAAsyncSelect(s, hWnd, wm_msg, FD_READ);
WSAAsyncSelect(s, hWnd, wm_msg, FD_ACCEPT);
  1. 在同一个套接字上多次调用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`文件中:

  1. 在`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;
}
  1. 在窗口回调函数中判断当前消息是否为自定义消息:
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)处理网络事件

  • 在界面上显示连接状态和消息

相关推荐
梦幻通灵31 分钟前
Java Stream两种list判断字符串是否存在方案
java·windows·list
要下雨了吗1 小时前
C语言三大程序结构 & 单分支语句
c语言·c++·visual studio
mrbone112 小时前
C++-C++中的几种cast
java·开发语言·c++
小宋要上岸2 小时前
基于TCP/QT/C++的网络调试助手测试报告
网络·c++·qt·网络协议·tcp/ip
muttry2 小时前
电脑磁盘分盘
windows
Aurora_wmroy2 小时前
算法竞赛备赛——【数据结构】栈&单调栈
数据结构·c++·算法·蓝桥杯
山遥路源3 小时前
3.22刷题
c++
空雲.3 小时前
ABC 369
数据结构·c++·算法
web150854159353 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat
kfhj3 小时前
C++ 关系运算符重载和算术运算符重载的例子,运算符重载必须以operator开头
c++