1. 程式人生 > >網路IO模型-非同步選擇模型(Delphi版)

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

其實關於這個模型,網路上也有一個案例說明 > 老陳使用了微軟公司的新式信箱。這種信箱非常先進,一旦信箱裡有新的信件,蓋茨就會給老陳打電話:喂,大爺,你有新的信件了!從此,老陳再也不必頻繁上下樓檢查信箱了,牙也不疼了,微軟提供的WSAAsyncSelect模型就是這個意思。 非同步選擇(WSAAsyncSelect)模型是一個有用的非同步 I/O 模型。利用這個模型,應用程式可在一個套接字上,接收**以 Windows 訊息為基礎**的網路事件通知。具體的做法是在建好一個套接字後,呼叫WSAAsyncSelect函式。該模型的核心即是WSAAsyncSelect函式。 WSAAsyncSelect函式定義如下: c++ ```c++ int WSAAPI WSAAsyncSelect( SOCKET s, HWND hWnd, u_int wMsg, long lEvent ); ``` delphi ```pascal 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訊息的形式,接收網路事件通知。視窗例程通常定義如下: ```c++ 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 ## 程式碼實現 ```pascal 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. ``` > 客戶端程式碼不變,可以使用相同模型也可以不