1. 程式人生 > >二、非同步選擇模型(WSAAsyncSelect)

二、非同步選擇模型(WSAAsyncSelect)

█ 非同步選擇(WSAAsyncSelect)模型是一個有用的非同步 I/O 模型。利用這個模型,應用程式可在一個套接字上,
接收以 Windows 訊息為基礎的網路事件通知。具體的做法是在建好一個套接字後,呼叫WSAAsyncSelect函式。
該模型的核心即是WSAAsyncSelect函式。


█ 要想使用 WSAAsyncSelect 模型,在應用程式中,首先必須用CreateWindow函式建立一個視窗,再為該視窗提供一個視窗例程函式(WinProc)。


█ WSAAsyncSelect 的函式原型如下:
int WSAAsyncSelect(
  __in          SOCKET s,
  __in          HWND hWnd,
  __in          unsigned int wMsg,
  __in          long lEvent
);
● s 引數指定的是我們感興趣的那個套接字。
● hWnd 引數指定一個視窗控制代碼,它對應於網路事件發生之後,想要收到通知訊息的那個視窗。
● wMsg 引數指定在發生網路事件時,打算接收的訊息。該訊息會投遞到由hWnd視窗控制代碼指定的那個視窗。
(通常,應用程式需要將這個訊息設為比Windows的WM_USER大的一個值,避免網路視窗訊息與系統預定義的標準視窗訊息發生混淆與衝突)
● lEvent 引數指定一個位掩碼,對應於一系列網路事件的組合,大多數應用程式通常感興趣的網路事件型別包括: 
FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE。當然,到底使用FD_ACCEPT,還是使用FD_CONNECT型別,
要取決於應用程式的身份是客戶端,還是伺服器。如應用程式同時對多個網路事件有興趣,只需對各種型別執行一次簡單的按位OR(或)運算,
然後將它們分配給lEvent就可以了,例如:
WSAAsyncSeltct(s, hwnd, WM_SOCKET, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);
解釋說明:我們的應用程式以後便可在套接字s上,接收到有關連線、傳送、接收以及套接字關閉這一系列網路事件的通知。


█ 注意 ①:
多個事件務必在套接字上一次註冊!
另外還要注意的是,一旦在某個套接字上允許了事件通知,那麼以後除非明確呼叫closesocket命令,
或者由應用程式針對那個套接字呼叫了WSAAsyncSelect,從而更改了註冊的網路事件型別,否則的話,
事件通知會永遠有效!若將lEvent引數設為0,效果相當於停止在套接字上進行的所有網路事件通知。


█ 注意 ②:
若應用程式針對一個套接字呼叫了WSAAsyncSelect,那麼套接字的模式會從“鎖定”變成“非鎖定”。
這樣一來,如果呼叫了像WSARecv這樣的Winsock函式,但當時卻並沒有資料可用,那麼必然會造成呼叫的失敗,並返回WSAEWOULDBLOCK錯誤。
為防止這一點,應用程式應依賴於由WSAAsyncSelect的uMsg引數指定的使用者自定義視窗訊息,來判斷網路事件型別何時在套接字上發生;而不應盲目地進行呼叫。


FD_READ應用程式想要接收有關是否可讀的通知,以便讀入資料
FD_WRITE應用程式想要接收有關是否可寫的通知,以便寫入資料
FD_ACCEPT應用程式想接收與進入連線有關的通知
FD_CONNECT應用程式想接收與一次連線完成的通知
FD_CLOSE應用程式想接收與套接字關閉的通知


█ 應用程式在一個套接字上成功呼叫了WSAAsyncSelect之後,會在與hWnd視窗控制代碼對應的視窗例程中,以Windows訊息的形式,接收網路事件通知。
視窗例程通常定義如下:
LRESULT CALLBACK WindowProc( 
    HWND hwnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
);
● hWnd 引數指定一個視窗的控制代碼,對視窗例程的呼叫正是由那個視窗發出的。
● uMsg 引數指定需要對哪些訊息進行處理。這裡我們感興趣的是WSAAsyncSelect呼叫中定義的訊息。
● wParam 引數指定在其上面發生了一個網路事件的套接字。假若同時為這個視窗例程分配了多個套接字,這個引數的重要性便顯示出來了。
● lParam引數中,包含了兩方面重要的資訊。其中, lParam的低字(低位字)指定了已經發生的網路事件,而lParam的高字(高位字)包含了可能出現的任何錯誤程式碼。


█ 步驟:網路事件訊息抵達一個視窗例程後,應用程式首先應檢查lParam的高字位,以判斷是否在網路錯誤。
這裡有一個特殊的巨集: WSAGETSELECTERROR,可用它返回高字位包含的錯誤資訊。
若應用程式發現套接字上沒有產生任何錯誤,接著便應調查到底是哪個網路事件型別,具體的做法便是讀取lParam低字位的內容。
此時可使用另一個特殊的巨集:WSAGETSELECTEVENT,用它返回lParam的低字部分。


█ 注意 ③:應用程式如何對 FD_WRITE 事件通知進行處理。
只有在三種條件下,才會發出 FD_WRITE 通知:
■ 使用 connect 或 WSAConnect,一個套接字首次建立了連線。
■ 使用 accept 或 WSAAccept,套接字被接受以後。
■ 若 send、WSASend、sendto 或 WSASendTo 操作失敗,返回了 WSAEWOULDBLOCK 錯誤,而且緩衝區的空間變得可用。


因此,作為一個應用程式,自收到首條 FD_WRITE 訊息開始,便應認為自己必然能在一個套接字上發出資料,
直至一個send、WSASend、sendto 或 WSASendTo 返回套接字錯誤 WSAEWOULDBLOCK。

經過了這樣的失敗以後,要再用另一條 FD_WRITE 通知應用程式再次傳送資料。

用例:

WSAAsyncSelect(pThis->m_SockListen, pThis->GetSafeHwnd(), WM_SOCKET, FD_ACCEPT | FD_CLOSE);//WM_SOCKET為自定義訊息#define WM_SOCKET WM_USER+100
//override視窗過程函式
LRESULT CServerDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
	switch(message)
	{
	case WM_SYSCOMMAND:
		if (wParam == SC_CLOSE) {
			closesocket(m_SockClient);
			closesocket(m_SockListen);
			WSACleanup();
		}
		break;
	case WM_SOCKET:
		if (WSAGETSELECTERROR(lParam)) {
			--m_ClientNums;
			closesocket(wParam);
			break;
		}
		switch(WSAGETSELECTEVENT(lParam))
		{
		case FD_ACCEPT:
			{
				if (m_ClientNums >= 1) {
					break;
				}
				m_SockClient = accept(wParam, NULL, NULL);
				WSAAsyncSelect(m_SockClient, m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
				++m_ClientNums;
				break;
			}
		case FD_READ:
			{
				TCHAR szBuf[MAX_BUF_SIZE] = {0};
				recv(wParam, (char *)szBuf, MAX_BUF_SIZE, 0);
				ShowMsg(szBuf);
				break;
			}
		case FD_WRITE:
			wParam = wParam;
			break;
		case FD_CLOSE:
			--m_ClientNums;
			closesocket(wParam);
			break;
		}
	default:break;
	}

	return CDialog::WindowProc(message, wParam, lParam);
}