網路IO模型-非同步選擇模型(Delphi版)

其實關於這個模型,網路上也有一個案例說明

老陳使用了微軟公司的新式信箱。這種信箱非常先進,一旦信箱里有新的信件,蓋茨就會給老陳打電話:喂,大爺,你有新的信件了!從此,老陳再也不必頻繁上下樓檢查信箱了,牙也不疼了,微軟提供的WSAAsyncSelect模型就是這個意思。

非同步選擇(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說明的部分欄位,完整說明參閱://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版的我在網上沒找到啥有用資料

參考部落格://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.

客戶端程式碼不變,可以使用相同模型也可以不同模型

Tags: