十、基于I/O模型的網絡開發
接著上次的博客繼續分享:select模型
10.8 異步選擇模型WSAAsyncSelect
10.8.1 基本概念
-
WSAAsyncSelect模型是Windows socket的一個異步I/O 模型,利用這個模型,應用程序 可在一個套接字上接收以Windows 消息為基礎的網絡事件通知。
-
Windows sockets應用程序在 創建套接字后,調用WSAAsyncSelect 函數注冊感興趣的網絡事件,當該事件發生時Windows 窗口收到消息,應用程序就可以對接收到的網絡事件進行處理了。
-
利用WSAAsyncSelect 函數, 將socket 消息發送到hWnd 窗口上,然后在那里處理相應的FD_READ 、FD_WRITE 等消息。
-
WSAAsyncSelect 模型與select 模型的相同點是它們都可以對多個套接字進行管理。
-
但它 們也有不小的區別。首先WSAAsyncSelect 模型是異步的,且通知方式不同。更重要的一點是: WSAAsyncSelect 模型應用在基于消息的Windows 環境下,使用該模型時必須創建窗口,而 select 模型可以廣泛應用在UNIX/Linux 系統,使用該模型不需要創建窗口。最后一點區別是:應用程序在調用WSAAsyncSelect 函數后,套接字就被設置為非阻塞狀態;而使用select 函數 不改變套接字的工作方式。
-
由于要關聯一個Windows 窗口來接收消息,因此如果處理成千上萬的套接字就力不從心 了。這也是該模型的一個缺點。另外,由于調用WSAAsyncSelect 后,套接字被設為非阻塞模 式,那么其他一些函數調用不一定能成功返回,必須要對這些函數的調用返回做處理。對于這 一點,可以從accept() 、receive() 和 send() 等函數的調用中得到驗證。
-
WSAAsyncSelect模型也有其優點,即提供了讀寫數據能力的異步通知。而且,該模型為 確保接收所有數據提供了很好的機制,通過注冊FD_CLOSE網絡事件,可以從容關閉服務器與客戶端的連接,保證了數據的全部接收。
10.8.2 WSAAsyncSelect函數
WSAAsyncSelect函數會自動將套接字設置為非阻塞模式,并且把發生在該套接字上且是 你所感興趣的事件以Windows 消息的形式發送到指定的窗口。
WSAAsyncSelect函數聲明如下:
int WSAAsyncSelect(in SOCKET s,in HWND hWnd,__in unsigned int wMsg,__in long lEvent);
-
s: 標識一個需要事件通知的套接口的描述符。
-
hWnd: 標識一個在網絡事件發生時需要接收消息的窗口句柄。
-
wMsg: 在網絡事件發生時要接收的消息。
-
IEvent: 位屏蔽碼,用于指明應用程序感興趣的網絡事件集合。IEvent 參數可取下列 值:
-
- FD_READ: 欲接收讀準備好的通知。發生FD_READ 的條件是:
-
-
- 調 用recv 或 者recvfrom 函數后,仍然有數據可讀。
-
-
-
- 調用WSAAsyncSelect 有數據可讀。
-
-
- FD_WRITE: 欲接收寫準備好的通知。發生FD_WRITE 的條件是:
-
-
- 當調用WSAAsyncSelect 函數時,如果調用能夠發送數據。
-
-
-
- 調用connect 或 者accept 函數后,當連接已經建立時。
-
-
-
- 調用send 或 者sendto, 返 回WSAWOULDBLOCK 錯誤碼,再次調用send 或 者sendto 函數可能成功時。
-
-
- FD_OOB: 欲接收帶邊數據到達的通知。
-
- FD_ACCEPT: 欲接收將要連接的通知。
-
- FD_CONNECT: 欲接收已連接好的通知。
-
- FD_CLOSE: 欲接收套接口關閉的通知。發生FD CLOSE 的條件是:
-
-
- 當調用WSAAsyncSelect 函數時,套接字連接關閉時。
-
-
-
- 對方執行從容關閉后,沒有數據可讀時,如果數據已經到達并等待讀取,FD_CLOSE 事件不會被發送,直到所有數據都被接收。
-
-
-
- 調用shutdown 函數執行從容關閉,對方應答FIN 后,此時無數據可讀。
-
-
-
- 對方結束了連接,并且lparam 包 含WSAECONNRESET 錯誤時。
-
-
- FD_QOS: 欲接收套接字服務質量發生變化的通知。
-
- FD_GROUP_QOS: 欲接收套接字組服務質量發生變化的通知。
-
- FD_ADDRESS_LIST_CHANGE: 欲接收針對套接字的協議簇,本地地址列表發生變化的通知。
-
- FD ROUTING INTERFACE CHANGE: 欲在指定方向上與路由接口發生變化的通知 。
如果函數成功就返回0,如果出錯就返回 SOCKET_ERROR,此時可用函數 WSAGetLastError 獲取更多信息。
可根據需要同時注冊多個網絡事件,這時要把網絡事件類型執行按位或(OR) 運算,然 后將它們分配給 IEvent 參數。例如,應用程序希望在套接字上接收連接完成、數據可讀和套 接字關閉的網絡事件,可調用如下函數:
WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_CONNECT | FD_READ | FD_CLOSE);
當該套接字連接完成、有數據可讀或者套接字關閉的網絡事件發生時,就會有 WM_SOCKET消息發送給窗口句柄為hwnd 的窗口。
值得注意的是,啟動一個 WSAAsyncSelect 將使為同一個套接口啟動的所有先前的 WSAAsyncSelect 作廢。
使用WSAAsyncSelect 函數需要注意的地方:
- (1)調用該函數后,套接字被設置為非阻塞模式,要想恢復為阻塞模式,必須再次調用 該函數,取消掉注冊過的事件,再調用ioctlsocket 設為阻塞模式。如果要取消所有的網絡事件通知,告知windows sockets實現不再為該套接字發送任何網
絡事件相關的消息,要以參數IEvent 值為0調用函數,即
WSAAsyncSelect(s, hwnd,0,0)
盡管應用程序調用上述函數取消了網絡事件通知,但是在應用程序消息隊列中,可能還有 網絡消息在排隊。所以調用上述函數取消網絡事件消息后,應用程序還應該繼續準備接收網絡 事 件 。
- (2)消息函數的wParam 參數為事件發生的套接字,LParam 對應錯誤消息和相應的事件, 可以調用宏WSAGETSELECTERROR(IParam) 、WSAGETSELECTEVENT(IParam) 來獲取具體 的 信 息 。
- (3)多次調用WSAAsyncSelect 函數在同一個套接字上注冊不同的事件(多次調用采用 同樣或者不同樣的消息),最后一次調用將取消前面注冊的事件。比如前后兩次調用:
WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_READ);
WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_WRITE);
此時雖然消息相同,都是WM_SOCKET, 但是應用程序只能接收到FD_WRITE 網絡事件。
還有一種情況是消息不同、網絡事件也不同,比如:
WSAAsyncSelect(s, hwnd, wMsg1, FD_READ);
WSAAsyncSelect(s, hwnd, wMsg2, FD_WRITE);
第二次函數調用依舊將會取消第一次函數調用的作用,只有 FD_WRITE 網絡事件通過wMsg2 通知到窗口。
這也是很多初學者發現接收不到網絡事件的原因。因為最后一次調用將取消前面注冊的事 件。
- (4)使用accept 函數建立的套接字與監聽套接字具有同樣的屬性,也就是說,在監聽套 接字上注冊的事件同樣會對建立連接的套接字起作用,如果一個監聽套接字請求 FD_READ 和 FD_WRITE 網絡事件,那么在該監聽套接字上接受的任何套接字也會請求 FD_READ 和 FD_WRITE 網絡事件,以及發送同樣的消息。
我們一般會在監聽套接字建立連接后重新為其注冊事件。
-
(5)為一個FD_READ網絡事件不要多次調用recv(函數,如果應用程序為一個FD_READ 網絡事件調用多個recv()函數,就會使得該應用程序收到多個FD_READ 網絡事件。如果在一 次接收FD_READ 網絡事件時需要調用多次 recv()函數,應用程序就應該在調用recv()函數之 前關閉FD_READ消息。
-
(6)使用FD_CLOSE 事件來判斷套接字是否已經關閉,錯誤代碼指示套接字是從容關閉 還是硬關閉:錯誤碼為0,代表從容關閉;錯誤碼為WSAECONNERESET, 則為硬關閉。如 果套接字從容關閉,數據全部接收,應用程序就會收到FD_CLOSE。
-
(7)發送數據出現失敗。一個應用程序當接收到第一個FD_WRITE 網絡事件后,便認為 在該套接字上可以發送數據。當調用輸出函數發送數據時,會收到 WSAEWOULDBLOCKE 錯誤。經過這樣的失敗后,要在下一次接收到FD_WRITE網絡事件后再次發送數據,才能夠 將數據成功發送。
10.8.3 實戰WSAAsyncSelect 模型
WSAAsyncSelect 傳參需要窗口句柄。為了簡化代碼,這里直接創建了一個mfc 對話框程 序,用m_hwnd 給 WSAAsyncSelect 傳參。對話框類名為WSAAsyncSelecDlg。
服務端
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")#define WM_SOCKET (WM_USER + 101)//-------------------窗口過程----------------------
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{switch (uMsg){case WM_SOCKET:{SOCKET ss = wParam; // wParam 參數標志了網絡事件發生的套接口long event = WSAGETSELECTEVENT(lParam); // 事件int error = WSAGETSELECTERROR(lParam); // 錯誤碼if (error){closesocket(ss);return 0;}switch (event){case FD_ACCEPT: //-----①連接請求到來{sockaddr_in Cadd;int Cadd_len = sizeof(Cadd);SOCKET sNew = accept(ss, (sockaddr*)&Cadd, &Cadd_len);if (sNew == INVALID_SOCKET){MessageBox(hwnd, L"調用accept()失敗!", L"標題欄提示", MB_OK);}else{WSAAsyncSelect(sNew, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);}} break;case FD_READ: //-----②數據發送來{char cbuf[256];memset(cbuf, 0, sizeof(cbuf));int cRecv = recv(ss, cbuf, sizeof(cbuf), 0);if ((cRecv == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET) || cRecv == 0){MessageBox(hwnd, L"調用recv()失敗!", L"標題欄提示", MB_OK);closesocket(ss);}else if (cRecv > 0){// 轉換消息為寬字符wchar_t wbuf[256];MultiByteToWideChar(CP_ACP, 0, cbuf, -1, wbuf, sizeof(wbuf) / sizeof(wchar_t));MessageBox(hwnd, wbuf, L"收到的信息", MB_OK);char Sbuf[] = "Hello client! I am server";int isend = send(ss, Sbuf, sizeof(Sbuf), 0);if (isend == SOCKET_ERROR || isend <= 0){MessageBox(hwnd, L"發送消息失敗!", L"標題欄提示", MB_OK);}else{MessageBox(hwnd, L"已經發信息到客戶端!", L"標題欄提示", MB_OK);}}} break;case FD_CLOSE: //----③關閉連接{closesocket(ss);}break;}}break;case WM_CLOSE:if (IDYES == MessageBox(hwnd, L"是否確定退出?", L"message", MB_YESNO))DestroyWindow(hwnd);break;case WM_DESTROY:PostQuitMessage(0);break;default:return DefWindowProc(hwnd, uMsg, wParam, lParam);}return 0;
}//----------------WinMain()函數------------------
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{WNDCLASS wc;wc.style = CS_HREDRAW | CS_VREDRAW;wc.lpfnWndProc = WindowProc;wc.cbClsExtra = 0;wc.cbWndExtra = 0;wc.hInstance = hInstance;wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);wc.hCursor = LoadCursor(NULL, IDC_ARROW);HBRUSH hbrush = CreateSolidBrush(RGB(0, 128, 25));wc.hbrBackground = hbrush;wc.lpszMenuName = NULL;wc.lpszClassName = L"Test";//---注冊窗口類(使用寬字符版本函數)---- RegisterClassW(&wc);//---創建窗口---- HWND hwnd = CreateWindowW(L"Test", L"WSAAsyncSelect模型-服務端窗口", WS_SYSMENU, 300, 0, 600, 400, NULL, NULL, hInstance, NULL);if (hwnd == NULL){MessageBoxW(NULL, L"創建窗口出錯", L"標題欄提示", MB_OK);return 1;}//---顯示窗口---- ShowWindow(hwnd, SW_SHOWNORMAL);UpdateWindow(hwnd);//---初始化WSA---WSADATA wsaData;WORD wVersionRequested = MAKEWORD(2, 2);if (WSAStartup(wVersionRequested, &wsaData) != 0){MessageBoxW(NULL, L"WSAStartup() Failed", L"調用失敗", 0);return 1;}SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (s == INVALID_SOCKET){MessageBoxW(NULL, L"socket() Failed", L"調用失敗", 0);return 1;}sockaddr_in sin;sin.sin_family = AF_INET;sin.sin_port = htons(6000);sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");if (bind(s, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR){MessageBoxW(NULL, L"bind() Failed", L"調用失敗", 0);return 1;}if (listen(s, 3) == SOCKET_ERROR){MessageBoxW(NULL, L"listen() Failed", L"調用失敗", 0);return 1;}elseMessageBoxW(hwnd, L"進入監聽狀態!", L"標題欄提示", MB_OK);WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);//---消息循環----MSG msg;while (GetMessageW(&msg, 0, 0, 0)){TranslateMessage(&msg);DispatchMessageW(&msg);}closesocket(s);WSACleanup();return msg.wParam;
}
客戶端
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include<stdlib.h>
#include<WINSOCK2.H>
#include <windows.h>
#include <process.h> #include<iostream>
#include<string>
using namespace std;#define BUF_SIZE 64
#pragma comment(lib,"wS2_32.lib")void recv(PVOID pt)
{SOCKET sHost = *((SOCKET*)pt);while (true){char buf[BUF_SIZE];//清空接收數據的緩沖區memset(buf, 0, BUF_SIZE);int retVal = recv(sHost, buf, sizeof(buf), 0);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();//無法立即完成非阻塞Socket上的操作if (err == WSAEWOULDBLOCK){Sleep(1000);//printf("\nwaiting reply!");continue;}else if (err == WSAETIMEDOUT || err == WSAENETDOWN || err == WSAECONNRESET)//已建立連接{printf("recv failed!");closesocket(sHost);WSACleanup();return;}}Sleep(100);printf("\n%s", buf);//break;}
}int main()
{WSADATA wsd;SOCKET sHost;SOCKADDR_IN servAddr;//服務器地址int retVal;//調用Socket函數的返回值char buf[BUF_SIZE];//初始化Socket環境if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0){printf("WSAStartup failed!\n");return -1;}sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//設置服務器Socket地址servAddr.sin_family = AF_INET;servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//在實際應用中,建議將服務器的IP地址和端口號保存在配置文件中servAddr.sin_port = htons(6000);//計算地址的長度int sServerAddlen = sizeof(servAddr);//調用ioctlsocket()將其設置為非阻塞模式int iMode = 1;retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) & iMode);if (retVal == SOCKET_ERROR){printf("ioctlsocket failed!");WSACleanup();return -1;}//循環等待while (true){//連接到服務器retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));if (SOCKET_ERROR == retVal){int err = WSAGetLastError();//無法立即完成非阻塞Socket上的操作if (err == WSAEWOULDBLOCK || err == WSAEINVAL){Sleep(1);printf("check connect!\n");continue;}else if (err == WSAEISCONN)//已建立連接{break;}else{printf("connection failed!\n");closesocket(sHost);WSACleanup();return -1;}}}unsigned long threadId = _beginthread(recv, 0, &sHost);//啟動一個線程接收數據的線程 while (true){//向服務器發送字符串,并顯示反饋信息printf("input a string to send:\n");std::string str;//接收輸入的數據std::cin >> str;//將用戶輸入的數據復制到buf中ZeroMemory(buf, BUF_SIZE);strcpy_s(buf, str.c_str());if (strcmp(buf, "quit") == 0){printf("quit!\n");break;}while (true){retVal = send(sHost, buf, strlen(buf), 0);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK){//無法立即完成非阻塞Socket上的操作Sleep(5);continue;}else{printf("send failed!\n");closesocket(sHost);WSACleanup();return -1;}}break;}}return 0;
}
監聽狀態:
收到客戶端消息:
參考書籍:《Visual C++2017 網絡編程實戰》