• 网络程序设计——异步选择模型(基于消息的选择、基于事件的选择)


    目录

    一、异步选择模型

    1、基于消息的选择模型

    (1)WSAAsyncSelect模型

    (2) WSAAsyncSelect()函数

    (3)使用 WSAAsyncSelect模型接收数据的过程

     (4)WSAAsyncSelect模型的编程方法

    (5) 使用WSAAsyncSelect模型实现服务器

    2、基于事件的选择模型

    (1)WinSock中的网络事件与事件对象函数

     ①WSACreateEvent()函数

    ②WSAResetEvent()函数

    ③WSASetEvent()函数 

    ④WSACloseEvent() 函数 

    (2) WSAEventSelect模型的函数

     (3)网络事件等待函数WSAWaitForMultipleEvents()       

      (4)网络事件枚举函数WSAEnumNetworkEvents()

     (5)WSAEventSelect模型的编程方法

    一、异步选择模型

    1、基于消息的选择模型

    (1)WSAAsyncSelect模型

    •        阻塞模型是在不知I/O事件是否发生的情况下,应用程序会按自己既定的流程主动去执行IO操作,结果通常是阻塞并等待相应事件发生;
    •       非阻塞模型也是在不知I/O事件是否发生的情况下,应用程序按自己既定的流程,反复执行IO操作直到操作成功(I/O事件发生);
    •       Select模型则是在不知I/O事件是否发生的情况下,应用程序按既定流程调用select函数主动检查关心的IO事件是否发生,如果没有发生则select()函数也是阻塞等待。

          共同特点:不管 I/O事件是否发生,应用程序都会按既定流程主动试着进行I/O操作,而且直至操作成功才会罢休,因此这三种套接字模型都属于同步模型

          尽管非阻塞模型和Select模型一次能够尝试对多个套接字进行I/O操作,要比阻塞模型效率高很多,但应用程序一旦开始I/O操作,则I/O操作完成之前都是无法进行其它操作。

          解决这一问题的方法是采用异步I/O模型。

    •       异步套接字I/O模型中,当网络I/O事件发生时,系统将采用某种机制通知应用程序,应用程序只有在收到事件通知时才调用相应的套接字函数进行I/O操作。

          WSAAsyncSelect模型和WSAEventSelect模型都属于异步I/O模型,二者的差别在于系统通知应用程序的方法不同。

    (2) WSAAsyncSelect()函数

    WSAAsyncSelect()函数,其格式如下。

    WSAAsyncSelect(s, hWnd, 0, 0);

    •       s为要被取消注册网络事件的套接字,hWnd为注册这些事件时指定的接收网络事件消息的窗口的句柄。
    •       取消网络事件的注册之后,系统将不再为该套接字发送任何与网络事件相关的消息。
    •       需要特别强调,WSAAsyncSelect模型应用在Windows环境下,使用该模型时必须创建窗口。
    •       而Slelect模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。

          应用程序调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式,而应用程序中调用select()函数后,并不能改变该套接字的工作方式。

    •        WSAAsyncSelect模型是基于Windows的消息机制实现的,当网络事件发生时,Windows系统将发送一条消息给应用程序,应用程序将根据消息做出相应的处理。该模型的核心是WSAAsyncSelect()函数。
    •        WSAAsyncSelect()函数的主要功能:是为指定的套接字注册一个或多个应用程序需要关注的网络事件。
    •        WSAAsyncSelect模型是非阻塞的,在应用程序中调用WSAAsyncSelect()函数后,该函数将向系统注册完成参数lEvent指定的网络事件后立即返回。
    •        WSAAsyncSelet模型是异步的,当已被注册的网络事件发生时,系统将向应用程序发送消息,该消息将由参数hWnd指定的窗口的相应消息处理函数进行处理,编写相应的消息处理函数是程序编写的主要工作之一。

          注意:注册网络事件时需要指定事件发生时需要发送的消息以及处理该消息的窗口的句柄。

          程序运行时,一旦被注册的事件发生,系统将向指定的窗口发送指定的消息。

    1. int WSAAsyncSelect
    2. {
    3. SOCKET s, //需要事件通知的套接字
    4. HWND hWnd,//当网络事件发生时接收消息的窗口句柄
    5. unsigned int wMsg, //当网络事件发生时向窗口发送的用户自定义消息
    6. long lEvent //要注册的应用程序感兴趣的套接字s的网络事件集合
    7. };

       函数返回值:应用程序感兴趣的事件注册成功,则返回0;如果注册失败,则返回SOCKET_ERROR。

           常用的网络事件包括FD_READ网络事件、FD_WRITE事件、FD_ACCEPT事件、FD_CONNECT事件、FD_CLOSE事件等。    

    •      FD_READ事件:读数据就绪的通知事件。事件触发时调用recv(), recvfrom(), WSARecv(), WSARecvfrom()。
    •      FD_WRITE事件:写数据就绪的通知事件。事件触发时调用send(), sendto(), WSASend(), WSASendto()。
    •       FD_ACCEPT事件:当前有连接请求需要接受。事件触发时调用accept(), WSAAccept()。
    •       FD_CONNECT事件:调用connect()函数后,建立连接完成。
    •       FD_CLOSE事件:仅对面向连接套接字有效,收到套接字关闭时触发。

       

    Tips:

    在VC++2017不鼓励使用 WSAAsyncSelect()模型,若使用编译器将发出错误警告并停止编译,如果要关闭错误警告继续编译,需要在头文件stdAfx.h中添加如下宏定义:

    • #define _WINSOCK_DEPRECATED_NO_WARNINGS

    或者在调用该函数的CPP文件头部使用以下预处理命令:

    • #pragma warning(disable : 4996)

    (3)使用 WSAAsyncSelect模型接收数据的过程

    •        调用recv()函数接收数据前,首先调用WSAAsyncselect()函数注册网络事件、事件发生时发出的用户自定义消息及处理消息的窗口。
    •        当系统收到数据时,系统将向应用程序发送消息。
    •        应用程序接收到这个消息后,将在消息对应的消息处理函数中调用recv()函数接收数据并处理数据。

     (4)WSAAsyncSelect模型的编程方法

         WSAAsyncSelect模型是基于Windows消息机制的,而其WSAAsyncSelect()函数要求消息的接收对象必须是一个窗口,因此基于WSAAsyncSelect模型的应用程序一般都是图形界面的窗口应用程序。

      程序的编写可以分为两大部分:建立并完善应用程序框架、编写消息处理函数。

       ① 第一步建立并完善应用程序框架需要完成如下任务:

    (1)使用应用程序向导创建对话框应用程序框架;

    (2)设计程序界面,主要是绘制控件并设置相关属性等;

    (3)为相关控件添加控件变量;

    (4)将通信所必须的套接字变量作为成员变量添加到窗口类中;

    (5)添加WSAAsyncSelect()函数在为套接字注册网络事件时发送的自定义消息;

    (6)在窗口类的成员函数OnInitDialog()中添加程序代码,完成创建套接字、给套接字绑定地址、使套接字处于监听状态、调用WSAAsyncSelect()函数为套接字注册网络事件等功能。

      ②第二步编写消息处理函数是程序设计的主要工作,除了编写相关控件消息的处理函数外,最主要的就是为套接字编写与网络事件关联的自定义消息的处理函数,在这些处理函数中要调用相关的套接字函数完成相关的IO处理。

          网络事件消息的处理函数具有类似下面代码所示的原型。

           afx_msg LRESULT OnSocketMsg(WPARAM wParam, LPARAM lParam);

          wParam参数存放发生网络事件的套接字的句柄,lParam参数的低16位存放的是发生的网络事件,高16位则用于存放网络事件发生错误时的错误码。

         函数的参数个数和类型是由系统规定的,它们的值在因消息到达而触发函数运行时由系统传入。

    (5) 使用WSAAsyncSelect模型实现服务器

          为了配合基于消息的选择模型,服务器的设计采用MFC对话框类程序。该程序的客户端仍然沿用标准客户端StdClient.exe,或者可以开发一个基于对话框的标准客户端。

    (1)使用“应用程序向导”创建“对话框应用程序”框架(项目名称为MsgSelectServer),其间应注意要在“高级功能”对话框中选中“Windows套接字”复选框,完成框架后要注意将项目所使用的字符集改为“多字节字符集”。

    (2)按照图示服务器程序界面,为程序添加控件并调整大小和位置。

    (3)通过类向导分别为列表框控件(用于显示和回射的内容)、“启动”按钮添加控件变量m_CListBox(类别为Control)、和m_StartButton(类别为Control)。

    (4)在应用程序的主头文件MsgSelectServer.h中的CMsgSelectServerApp类定义之前添加如下代码,定义两个自定义消息,这两个消息分别是注册监听套接字的FD_ACCEPT事件和注册已连接套接字的FD_READ事件时关联的消息。

    #define MsgAccept    WM_USER +100

    #define MsgRecv      WM_USER +101

    (5)通过类向导或者直接在文件MsgSelectServerDlg.h中的对话框类的定义中,添加如下自定义成员变量

    SOCKET m_ListenSocket;   //监听套接字变量

    SOCKET m_acceptSocket;  //存储连接建立后accept得到的套接字号

    struct sockaddr_in addr, client_addr;  //分别存储本地地址和客户端地址

    在MsgSelectServerDlg.h文件开始处添加常量定义:

    #define PORT 8883  //监听端口

    #define  BUFFER_LEN 1000   //接收缓冲区长度

    (6)在MsgSelectServerDlg.cpp文件中的CMsgSelectServerDlg::OnInitDialog()函数中添加如下代码,来创建套接字、给套接字绑定地址、使套接字处于监听状态,并调用WSAAsyncSelect()函数,为监听套接字注册FD_ACCEPT异步事件。

    (7) 利用类向导添加自定义消息MsgAccept及其消息处理函数。该消息发出时,说明有客户的连接请求到达,因此,该消息处理函数调用accept()与客户建立连接,并为连接成功后得到的套接字注册FD_READ事件和MsgRecv消息。

    (8)利用类向导添加自定义消息MsgRecv及其消息处理函数。该消息发出时,说明有数据到达,因此,该消息处理函数调用recv()函数接收数据,并将数据添加到ListBox控件中显示。

     Tips:

    为了能让VC++2019能忽略对WSAAsyncSelect()函数的警告而顺利编译,在头文件stdafx.h中的预处理命令 #define _AFX_ALL_WARNINGS之下,添加如下宏定义:

    #define _WINSOCK_DEPRECATED_NO_WARNINGS

    在“调试”->"MsgSelectServer调试属性"->"C/C++"->“常规”->"sdl检查"->“否”

    2、基于事件的选择模型

    (1)WinSock中的网络事件与事件对象函数

    •         WSAEventSelect模型与WSAAsyncSelect一样都属于异步I/O模型,二者的不同之处在于网络事件发生时系统通知应用程序的形式不同。
    •        WSAAsyncSelect模型是基于Windows的消息机制的,网络事件发生时系统将以消息的形式通知应用程序,并且消息必须与窗口句柄相关联,因此程序必须要有窗口对象才行。
    •        WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象关联,当网络事件发生时,经由事件对象句柄通知应用程序。

     ①WSACreateEvent()函数

    • 功能是创建一个“人工重设模式”工作的事件对象,初始为“无信号”状态。
    • 函数原型:WSAEVENT  WSACreateEvent( void );
    • 该函数无参数。函数执行成功则返回事件对象句柄,否则返回WSA_INVALID_EVENT。其中WSAEVENT是事件对象句柄类型。
    • 该函数是CreateEvent()函数的扩展,但它创建的是人工重设模式的事件对象,而CreateEvent()函数创建的则是自动重设模式。如果程序需要一个自动重设模式的事件对象,可直接使用CreateEvent()函数。

    ②WSAResetEvent()函数

    • 该函数将事件对象从“有信号”状态更改为“无信号”状态。函数原型如下:
    • BOOL  WSAResetEvent(WSAEVENT hEvent);
    • –参数hEvent是要设置的事件对象的句柄。
    • –如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。

    ③WSASetEvent()函数 

    • 该函数将事件对象设置为“有信号”状态,函数原型:
    • BOOL  WSASetEvent(WSAEVENT hEvent);
    • 函数参数hEvent是要设置的事件对象的句柄。
    • 如果该函数调用成功,则函数返回TRUE;反之函数返回FALSE。

    ④WSACloseEvent() 函数 

    • 释放事件对象占有的系统资源。该函数声明如下:
    •   BOOL  WSACloseEvent(WSAEVENT hEvent);
    • –函数参数hEvent是要释放的事件对象的句柄。
    • –如果函数调用成功,该函数返回TRUE;否则返回FALSE。

    (2) WSAEventSelect模型的函数

    •       WSAEventSelect()函数是WSAEventSelect模型的核心,该函数能够为套接字注册感兴趣的网络事件,将网络事件与事件对象关联起来。
    •       该模型的网络事件与WSAAsyncSelect模型完全相同。当为套接字注册的网络事件发生时,关联的事件对象将从“无信号”状态转变为“有信号”状态。
    1. //网络时间注册函数
    2. int WSAEventSelect
    3. {
    4. SOCKET s, //套接字
    5. WSAEVENT hEventObject, //事件对象句柄
    6. Long lNetworkEvents //应用程序感兴趣的网络事件集合
    7. };

    注意:

    • (1)如果应用程序同时对多个网络事件感兴趣,需要对网络事件类型执行按位OR(|)运算。

    例:应用程序对套接字s上的网络事件FD_READ和FD_CLOSE感兴趣,则可使用如下代码注册网络事件:

    SOCKET  s;
    WSAEVENT  hEvent;
    int  nReVal=WSAEventSelect(s, hEvent, FD_READ|FD_CLOSE);

    • (2)要取消为套接字注册的网络事件,必须再次调用WSAEventSelect()函数,并将InetworkEvents参数设置为0。

    例:要取消上面例子中为套接字s注册的网络事件,则只需使用如下一行代码:

    WSAEventSelect( s, hEvent, 0);

    • (3)应用程序调用WSAEventSelect()函数后,套接字将被自动设置为非阻塞模式。如果要将套接字设置为阻塞模式,必须先取消套接字上注册的网络事件,然后再调用ioctlsocket()函数将套接字设置为阻塞模式。如果不取消已注册的网络事件而直接调用ioctlsocket()函数来设置套接字为阻塞模式,将会失败并返回WSAEINVAL错误。

     (3)网络事件等待函数WSAWaitForMultipleEvents()       

    •        该函数的功能是等待与套接字关联的事件对象由“无信号”状态变为“有信号”状态。
    •        应用程序在调用WSAEventSelect()函数为套接字注册网络事件后调用该函数等待事件发生,事件发生前该函数将阻塞等待,直到等待的事件发生或设置的等待时间超时该函数才会返回。
    1. //函数原型
    2. DWORD WSAWaitForMultipleEvents
    3. {
    4. DWORD  cEvents,
    5. const WSAEVENT FAR * lphEvents,
    6. BOOL  fWaitAll,
    7. DWORD dwTimeout,
    8. BOOL   fAlertable
    9.  };
    10.     

    函数参数

    • cEvents:等待的事件对象句柄的数量。等待事件对象句柄数量至少为1,最多数量为WSA_MAXIMUM_WAIT_EVENTS,其值为64个;
    • lphEvents:指向事件对象句柄的指针,实际上是一个WSAEVENT类型的数组,参数cEvents则是数组中事件对象句柄的数量;
    • fWaitAll:该参数为TRUE,则该函数在lphEvents中所有的事件对象都转变为“有信号”状态时才返回,若为FALSE,则在其中一个事件句柄转变为“有信号”状态时就返回,并且返回值指示出促使函数返回的事件对象;
    • dwTimeout:函数阻塞等待的时间,单位为毫秒。超过该等待时间,即使没有满足fWaitAll参数指定的条件函数也会返回。如果该参数为0,则函数检查事件对象的状态并立即返回。如果该参数为WSA_INFINITE,则该函数会无限期等待下去,直到满足fWaitAll参数指定的条件。
    • fAlertable:该参数主要用于重叠I/O模型,在完成例程的处理过程使用。如果该参数为TRUE,说明该函数返回时完成例程已经被执行。如果该参数为FALSE,说明该函数返回时完成例程还没有执行。这里只要将该参数设置为FALSE就可以了。
    • 返回值代表使该函数返回的事件对象,分4种情况:

      (1)一个从WSA_WAIT_EVENT_0到WAS_WAIT_EVENT_0+cEvents-1范围的值,其中宏WAS_WAIT_EVENT_0的值为0,这时如果fWaitAll为TRUE,则表示所有事件对象都处于“有信号”状态,如果fWaitAll为FALSE,则返回值减去WAS_WAIT_EVENT_0,即为“有信号”事件对象在lphEvents数组中的序号;

      (2)WAIT_IO_COMPLETION,表示一个或者多个完成例程已经排队等待执行;

      (3)WSA_WAIT_TIMEOUT,表示函数调用超时,并且所有事件对象都没有处于“有信号”状态。

      (4)如果该函数调用失败,则返回值为WSA_WAIT_FAILED。

    (4)网络事件枚举函数WSAEnumNetworkEvents()

            用于获取套接字上发生的网络事件,同时清除系统内部的网络事件记录,如果需要,还可以重置事件对象。

    1. //函数原型
    2. int WSAEnumNetworkEvents
    3. {
    4. SOCKET s,
    5. WSAEVENT hEventObject,
    6. LPWSANETWORKEVENTS lpNetworkEvents
    7. };

    函数参数

    –s:发生网络事件的套接字句柄。

    –hEventObject:被重置的事件对象句柄(可选)

    –lpNetworkEvents:指向WSANETWORKEVENTS结构指针。在该结构中包含发生网络事件的记录和相关错误代码。

    1. //WSANETWORKEVENTS结构声明如下:
    2. typedef struct _WSANETWORKEVENTS {
    3.   long  lNetworkEvents;
    4.   int   iErrorCode[FD_MAX_EVENTS];
    5. } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

          lNetworkEvents:发生的网络事件。事件对象进入“有信号”状态时,在套接字上可能会发生多个网络事件,因此本参数记录的可能是多个网络事件。

          iErrorCode:包含网络事件错误代码的数组。错误代码与lnetworkEvents字段中的网络事件对应。在应用程序中,使用网络事件错误标识符对iErrorCode数组进行索引,检查是否发生了网络错误。这些标识符命名规则是在对应的网络事件后面添加“_BIT”。例如,对应于FD_READ网络事件的网络事件错误标识符为FD_READ_BIT,网络错误标识符代表是iErrorCode数组序号。

          该函数调用成功时返回值为0,反之为SOCKETS_ERROR。如果该函数返回SOCKET_ERROR错误,则事件对象不会被重置,网络事件也不会被清除。

          在调用WSAEnumNetworkEvents()函数时,如果参数hEventObjec不为NULL,则该参数指定的事件对象被置为“无信号”状态。通常参数hEventObjec被指定为与s指定的套接字相关联的事件对象,如果该参数设为NULL,则应用程序必须要调用WSAResetEvent()函数来将关联事件对象设置为“无信号”状态。

     (5)WSAEventSelect模型的编程方法

    WSAEventSelect模型编程的基本步骤是:

    ①创建一个事件对象数组,用于存放所有的事件对象;

    ②为相关的套接字创建事件对象;

    ③调用WSAEventSelect()函数将事件对象和需要关注套接字的网络事件关联起来;

    ④调用WSAWaitForMultipleEvents()函数等待网络事件发生。当有网络事件发生时关联的事件对象将变为“有信号”状态,此时在事件对象上等待的WSAWaitForMultipleEvents()函数将会立即返回;

    ⑤调用WSAEnumNetworkEvents函数,查询发生事件的事件对象获取具体发生的网络事件类型;

     ⑥根据网络事件类型调用相应的套接字函数进行处理。

           编写一个符合自定义标准的服务器端程序。要求服务器端使用WSAEventSelect模型,允许同时有多个客户接入,并能够持久与服务器通信。

    • 在调用WSAWaitForMultipleEvents()时,尽管fWaitALL参数指定为FALSE,但返回时它所等待的事件对象中仍可能有多个已检测到网络事件发生,这时返回的是所有已触发的事件对象中的在对象数组中下标值最小的,为了确保对所有的事件对象进行处理,需要对下标大于等于返回值nIndex的事件对象进行逐一检查,检查注册的网络事件是否发生,并对发生事件做出处理。
    • 这里判断一个事件对象是否有事件发生仍使用WSAWaitForMultipleEvents()函数,只不过其cEvents参数应指定为1,lphEvents参数为要查看的事件对象句柄变量的地址,而且也不能阻塞太长时间,所以参数dwTimeOUT需指定为有限值。

    FD_WRITE事件只有在以下情况下才会触发:

    • ①在建立连接成功时,客户服务器两端都会触发FD_WRITE事件;
    • ②send(WSASend)/sendto(WSASendTo)发送失败返回WSAEWOULDBLOCK,并且当缓冲区有可用空间时,则会触发FD_WRITE事件。

           第①种情况在本例中虽能频繁出现,但accept()返回的同时才获取套接字描述符,用于监视套接字的事件对象还没有创建,因此在这里用事件对象无法捕捉。在第②种情况中,由于只有套接字的发送缓冲区满的时候调用send()才会返回 WSAEWOULDBLOCK错误,因此 FD_WRITE 触发的前提是缓冲区要先被充满然后随着数据的发送又出现可用空间,而不是只要缓冲区中有可用空间。由于本例中只发送少量数据,套接字发送缓冲区不可能占满。

          从这里也可以知道,程序中需要发送数据时,应直接调用send()发送,而不应使用FD_WRITE触发。FD_WRITE一般用于发送大量数据时,调用send()函数发送数据失败返回WSAEWOULDBLOCK后,触发数据的再次发送。

    如有错误,敬请指正。

    您的收藏与点赞都是对我最大的鼓励和支持!

  • 相关阅读:
    贪心算法活动选择问题
    ggkegg | 用这个神包玩转kegg数据库吧!~(一)
    C++类和对象(中)【万字详解】
    嵌入式常见英文缩写
    2-38 JSP01
    [Linux]进程间通信(system V共享内存 | system V信号量)
    【数据结构】树的存储结构;树的遍历;哈夫曼树;并查集
    iOS 蓝牙扫描枪扫描内容不正确
    为什么要ROS2而不是对ROS1修修补补?
    pg分组过滤
  • 原文地址:https://blog.csdn.net/x20020402/article/details/128034148