网络IO模型-异步选择

Posted 跟着老侯玩编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络IO模型-异步选择相关的知识,希望对你有一定的参考价值。

其实关于这个模型,网络上也有一个案例说明

异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O 模型。利用这个模型,应用程序可在一个套接字上,接收以 Windows 消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型的核心即是WSAAsyncSelect函数。

WSAAsyncSelect函数定义如下:

c++

int WSAAPI WSAAsyncSelect(
  SOCKET s,
  HWND   hWnd,
  u_int  wMsg,
  long   lEvent
)
;

delphi

function WSAAsyncSelect(s: TSocket; hWnd: HWND; wMsg: u_int; lEvent: Longint): Integer; stdcall;

参数说明

参数名 具体含义
s 指定的是我们感兴趣的那个套接字。
hwnd 指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。
wMsg 指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。
lEvent 指定一个位掩码,对应于一系列网络事件的组合

注意事项

  • wMsg参数指定的消息通常是我们自定义的消息,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗口消息发生混淆与冲突。

  • lEvent参数指定的网络类型为:FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算就OK。

Value Meaning
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知

摘取MSDN说明的部分字段,完整说明参阅:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaasyncselect

  • 多个事件务必在套接字上一次注册!另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知

  • 若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“阻塞”变成“非阻塞”。这样以来,如果调用了像WSARecv这样的Winsock函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。

应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。窗口例程通常定义如下:

LRESULT CALLBACK WindowProc(
    HWND hwnd,      //指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。
    UINT uMsg,      //指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。
    WPARAM wParam,  //指定在其上面发生了一个网络事件的套接字。(假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。)
    LPARAM lParam   //包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。
)
;
  • Delphi对这个函数的参数做了封装,对应的结构体是TMessage,所以我们实际使用的只需要定义对应消息的处理函数即可

  • 大家可以看出上面的文字说明很明显是C++的,大部分内容我是摘抄自网络,Delphi版的我在网上没找到啥有用资料

参考博客:https://www.cnblogs.com/venow/archive/2012/06/09/2543053.html

代码实现


unit MainFrm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls;

const
  MY_WM_SOCKET = WM_USER + 55;

type
  TForm1 = class(TForm)
    Button1: TButton;
    StatusBar1: TStatusBar;
    Memo1: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    procedure WMSocket(var Msg: TMessage); message MY_WM_SOCKET;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses Winapi.WinSock2, ScktComp;

var
  WSAData: TWSAData;
  // 套接字对象,用于监听
  ClientSocket, Server: TSocket;
  ServerRecord: sockaddr_in;

procedure TForm1.Button1Click(Sender: TObject);
begin
  // 初始化版本库
  if WSAStartup(WINSOCK_VERSION, WSAData) <> ERROR_SUCCESS then
  begin

    WSACleanup
;
    Self.StatusBar1.Panels[0].Text := '初始化失败';
    Exit;
  end;

  // 初始化socket
  Server := socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
  // 创建失败
  if Server = INVALID_SOCKET then
  begin
    closesocket(Server);
    WSACleanup;
    Self.StatusBar1.Panels[0].Text := '初始化socket失败';
    Exit;
  end;

  // 指定IP、端口号和协议类型
  with ServerRecord do
  begin

    sin_family := PF_INET;
    sin_port := htons(10086);
    sin_addr.S_addr := inet_addr(PAnsiChar(AnsiString('127.0.0.1')));;
  end;

  // 绑定IP和端口号

  if bind(Server, TSockAddr(ServerRecord), SizeOf(ServerRecord)) SOCKET_ERROR
  then
  begin
    closesocket(Server)
;
    WSACleanup;
    Self.StatusBar1.Panels[0].Text := '端口号被占用';
    Exit;
  end;

  if listen(Server, SOMAXCONN) SOCKET_ERROR then
  begin
    closesocket(Server)
;
    WSACleanup;
    Self.StatusBar1.Panels[0].Text := '监听失败';
    Exit;
  end;
  // 核心函数
  WSAAsyncSelect(Server, Self.Handle, MY_WM_SOCKET, FD_ACCEPT or FD_READ or
    FD_WRITE or FD_CLOSE);
  // 禁用按钮
  Button1.Enabled := false;

end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  if WSACleanup <> ERROR_SUCCESS then

    Self.StatusBar1.Panels[0].Text := '初始化失败';

  if Server <> INVALID_SOCKET then

    closesocket(Server)
;
  Self.StatusBar1.Panels[0].Text := '网络库初始化成功';
end;

// 当产生网络消息的时候核心的处理函数
procedure TForm1.WMSocket(var Msg: TMessage);
begin
  if (Msg.Msg = MY_WM_SOCKET) then
    Self.StatusBar1.Panels[0].Text :
'网络消息';

  case WSAGetSelectEvent(Msg.LParam) of
    FD_ACCEPT:
      begin
        var
        AddSize :
= SizeOf(ServerRecord);

        ClientSocket := accept(Server, @ServerRecord, @AddSize);
        var
        CustomWinSocket := TCustomWinSocket.Create(ClientSocket);
        Form1.Memo1.Lines.Add('客户端IP:' + CustomWinSocket.RemoteAddress);
      end;
    FD_READ:
      begin

      end;
    FD_WRITE:
      begin

      end;
  end;
end;

end.

客户端代码不变,可以使用相同模型也可以不同模型

跟着老侯玩编程 发起了一个读者讨论 关于这个模型欢迎留下你的想法 精选讨论内容