十、基于I/O模型的網絡開發
10.9 事件選擇模型
10.0.1 基本概念
事件選擇(WSAEventSelect) 模型是另一個有用的異步 I/O 模型。和 WSAAsyncSelect 模 型類似的是,它也允許應用程序在一個或多個套接字上接收以事件為基礎的網絡事件通知,最 主要的差別在于網絡事件會投遞至一個事件對象句柄,而非投遞到一個窗口例程。
10.9.2 WSAEventSelect函數
WSAEventSelect 模型主要由函數WSAEventSelect 來實現。注意,這里用了“主要由”, 說明還有其他配套函數一起輔助來實現這個模型。
后面會講到其他函數。這里先看一下 WSAEventSelect。
WSAEventSelect 函數將一個已經創建好的事件對象(由WSACreateEvent 創建)與某個套 接字關聯在一起,同時注冊自己感興趣的網絡事件類型。WSAEventSelect 的函數聲明如下:
int WSAAPI WSAEventSelect(SOCKET S,WSAEVENT hEventObject, long lNetworkEvents);
-
其中,s 是套接字描述符;
-
hEventObject標識要與指定的網絡事件集關聯的事件對象的句 柄 ;
-
INetworkEvents指定應用程序感興趣的網絡事件組合的位掩碼。
-
如果函數成功,那么返回值為零;否則,將返回值SOCKET_ERROR, 并且可以通過調用 WSAGetLastError來獲取特定的錯誤號。
-
與select 和 WSAAsyncSelect 函數一樣,WSAEventSelect 通常用于確定何時可以進行數據 收發操作(確定調用send或 recv能立即成功的時間點)。如果時間點沒到,那么函數會返回 WSAEWOULDBLOCK, 此時我們要正確處理這個錯誤碼。
10.9.3 實 戰WSAEventSelect模型
事件選擇模型的基本思路是:為感興趣的一組網絡事件創建一個事件對象,再調用 WSAEventSelect 函數將網絡事件和事件對象關聯起來。當網絡事件發生時,Winsock 會使相 應的事件對象收到通知,在事件對象上等待的函數就會返回。之后,再調用 WSAEnumNetworkEvents函數便可獲取發生了什么網絡事件。
事件選擇模型寫的TCP 服務器實現的過程如下:
- (1)創建事件對象和套接字。創建一個事件對象的方法是調用 WSACreateEvent 函數, 它的定義如下:
WSAEVENT WSAAPI WSACreateEvent();
-
如果沒有發生錯誤,那么函數將返回事件對象的句柄;
-
否則,返回值為 WSA_INVALID_EVENT, 可以通過WSAGetLastError 函數獲取更多的錯誤信息。這個事件對 象創建后,其初始狀態為“未受信”,就是沒有收到通知狀態。
-
WSACreateEvent 創建的事件有兩種工作狀態以及兩種工作模式:工作狀態分別是“有信 號 " (signaled) 和“無信號" (nonsignaled), 工作模式包括“人工重設” (manual reset) 和“自動重設” (auto reset) 。WSACreateEvent 創建的事件開始是處于一種無信號的工作狀 態,并用一種人工重設模式來創建事件句柄。
-
(2)將事件對象與套接字關聯在一起,同時注冊自己感興趣的網絡事件類型(FD_READ、 FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE 等),這個過程通過函數 WSAEventSelect實現。
-
(3)調用事件等待函數WSAWaitForMultipleEvents在所有事件對象上等待,該函數返回后,我們就可以確認在哪些套接字上發生了網絡事件。 當一個或所有指定的事件對象處于信號狀態、超時或執行了 I/O 完成例程時,函數 WSAWaitForMultipleEvents返回,該函數聲明如下:
#include <winsock2.h>
#pragma comment(lib, "Ws232.lib")
DWORD WSAAPI WSAWaitForMultipleEvents(DWORD CEvents,const WSAEVENT *lphEvents, BOOL fWaitAll,DWORD dwTimeout,BOOL fAlertable);
- cEvents: 表 示lphEvent 所指數組中的事件對象句柄數,事件對象句柄的最大數量是 WSA_MAXIMUM_WAIT_EVENTS, 必須指定一個或多個事件。
- lphEvents: 指向事件對象句柄數組的指針,數組可以包含不同類型對象的句柄,如果 后面參數fWaitAll 設置為TRUE, 那么它不能包含同一句柄的多個副本,如果在等待 仍處于掛起狀態時關閉其中一個句柄,那么WSAWaitForMultipleEvents 的行為將不 可知。另外,句柄必須具有同步訪問權限。
- fWaitAll: 輸入參數,用于指定等待類型的值。如果賦值為TRUE, 那么當lphEvents 數組中所有對象的狀態都處于有信號時,函數將返回。注意,是所有對象都處于信號 狀態才返回。如果賦值為FALSE, 則當向任一事件對象發出信號時,函數返回。在 這一種情況下,返回值減去WSA_WAIT_EVENT_0 表示其狀態導致函數返回的事件 對象的索引。如果在調用期間有多個事件對象發出信號,那么返回值指示信號事件對 象的lphEvents 數組索引的最小值。
- dwTimeout: 超時時間,單位是毫秒。如果超時時間到,則函數返回,即使不滿足 fWaitAll 參數指定的條件。如果 dw Timeout參數為零,則函數將測試指定事件對象的 狀態并立即返回。如果dwTimeout 是 WSA_INFINITE, 則函數將永遠等待。
- fAlertable: 指定線程是否處于可警報的等待狀態,以便系統可以執行I/O 完成例程。 如果為TRUE, 則線程將處于可警報的等待狀態,并且當系統執行I/O 完成例程時, 函數可以返回。在這種情況下,將返回 WSA_WAIT_IO_COMPLETION, 并且尚未 發出正在等待的事件的信號。應用程序必須再次調用WSAWaitForMultipleEvents 函 數。如果為FALSE, 則線程不會處于可警報的等待狀態,也不會執行I/O 完成例程。
如果函數成功,那么返回值為以下值之一:
- WSA_WAIT_EVEN_0 到 (WSA_WAIT_EVENT_0+cEvents-1): 如果參數 fWaitAll 參數為 TRUE, 則返回值指示已向所有指定的事件對象發出信號。如果 fWaitAll 參數為FALSE, 則返回值減去WSA_WAIT_EVENT_0 表示其狀態導致函數 返回的事件對象的索引。如果在調用期間有多個事件對象發出信號,則返回值指示信 號事件對象的lphEvents 數組索引的最小值。
- WSA_WAIT_IO_COMPLETION:等待被執行的一個或多個I/O 完成例程結束。正在 等待的事件尚未發出信號,應用程序必須再次調用WSAWaitForMultipleEvents 函數。 只有fAlertable 參數為TRUE 時,才能返回此返回值。
- WSA_WAIT_TIMEOUT: 超時間隔已過,并且未滿足fWaitAll參數指定的條件,未
執行任何I/O完成例程。
如果函數失敗,則返回值為WSA_WAIT_FAILED。此時可以通過函數WSAGetLastError 獲取更多錯誤碼,常見錯誤碼如下:
-
WSANOTINITIALISED: 在調用本API 之前應成功調用WSAStartup()。
-
WSAENETDOWN: 網絡子系統失效。
-
WSA_NOT_ENOUGH_MEMORY: 無足夠內存完成該操作。
-
WSA_INVALID_HANDLE:lphEvents 數組中的一個或多個值不是合法的事件對象句柄。
-
WSA_INVALID_PARAMETER:cEvents參數未包含合法的句柄數目。
-
(4)檢測所指定套接字上發生網絡事件,然后處理發生的網絡事件,完畢繼續在事件對 象上等待。檢測所指定套接字上發生網絡事件是通過函數WSAEnumNetworkEvents 來實現, 該函數聲明如下:
#include <winsock2.h>
#pragma comment(lib, "Ws232.lib")int WSAAPI WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents);
- s: 套接字描述符。
- hEventObject: 標識要重置的關聯事件對象的可選句柄。
- lpNetworkEvents: 指 向WSANETWORKEVENTS 結構的指針,該結構由發生的網絡 事件和任何相關錯誤代碼的記錄填充。
如果操作成功,函數返回值為零;否則,將返回值SOCKET ERROR, 并且可以通過調用WSAGetLastError來獲取特定的錯誤碼。
以上4步是使用事件選擇模型的基本步驟。下面我們看一個實例。
服務端
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <winsock2.h>
#include <Windows.h>
#include <iostream>
#pragma comment(lib,"ws2_32.lib")using std::cout;
using std::cin;
using std::endl;
using std::ends;void WSAEventServerSocket()
{SOCKET server = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (server == INVALID_SOCKET) {cout << "創建SOCKET失敗!,錯誤代碼:" << WSAGetLastError() << endl;return;}int error = 0;sockaddr_in addr_in;addr_in.sin_family = AF_INET;addr_in.sin_port = htons(6000);addr_in.sin_addr.s_addr = INADDR_ANY;error = ::bind(server, (sockaddr*)&addr_in, sizeof(sockaddr_in));if (error == SOCKET_ERROR) {cout << "綁定端口失敗!,錯誤代碼:" << WSAGetLastError() << endl;return;}listen(server, 5);if (error == SOCKET_ERROR) {cout << "監聽失敗!,錯誤代碼:" << WSAGetLastError() << endl;return;}cout << "成功監聽端口 :" << ntohs(addr_in.sin_port) << endl;WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 事件對象數組SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; // 事件對象數組對應的SOCKET句柄int nEvent = 0; // 事件對象數組的數量 WSAEVENT event0 = ::WSACreateEvent();::WSAEventSelect(server, event0, FD_ACCEPT | FD_CLOSE);eventArray[nEvent] = event0;sockArray[nEvent] = server;nEvent++;while (true) {int nIndex = ::WSAWaitForMultipleEvents(nEvent, eventArray, false, WSA_INFINITE, false);if (nIndex == WSA_WAIT_IO_COMPLETION || nIndex == WSA_WAIT_TIMEOUT) {cout << "等待時發生錯誤!錯誤代碼:" << WSAGetLastError() << endl;break;}nIndex = nIndex - WSA_WAIT_EVENT_0;WSANETWORKEVENTS event;SOCKET sock = sockArray[nIndex];::WSAEnumNetworkEvents(sock, eventArray[nIndex], &event);if (event.lNetworkEvents & FD_ACCEPT) {if (event.iErrorCode[FD_ACCEPT_BIT] == 0) {if (nEvent >= WSA_MAXIMUM_WAIT_EVENTS) {cout << "事件對象太多,拒絕連接" << endl;continue;}sockaddr_in addr;int len = sizeof(sockaddr_in);SOCKET client = ::accept(sock, (sockaddr*)&addr, &len);if (client != INVALID_SOCKET) {cout << "接受了一個客戶端連接 " << inet_ntoa(addr.sin_addr) << ":" << ntohs(addr.sin_port) << endl;WSAEVENT eventNew = ::WSACreateEvent();::WSAEventSelect(client, eventNew, FD_READ | FD_CLOSE | FD_WRITE);eventArray[nEvent] = eventNew;sockArray[nEvent] = client;nEvent++;}}}else if (event.lNetworkEvents & FD_READ) {if (event.iErrorCode[FD_READ_BIT] == 0) {char buf[2500];ZeroMemory(buf, 2500);int nRecv = ::recv(sock, buf, 2500, 0);if (nRecv > 0) {cout << "收到一個消息 :" << buf << endl;char strSend[] = "hi,client,I am server, I recvived your message.";::send(sock, strSend, strlen(strSend), 0);}}}else if (event.lNetworkEvents & FD_CLOSE) {::WSACloseEvent(eventArray[nIndex]);::closesocket(sockArray[nIndex]);cout << "一個客戶端連接已經斷開了連接" << endl;for (int j = nIndex; j < nEvent - 1; j++) {eventArray[j] = eventArray[j + 1];sockArray[j] = sockArray[j + 1];}nEvent--;}else if (event.lNetworkEvents & FD_WRITE) {cout << "一個客戶端連接允許寫入數據" << endl;}} // end while::closesocket(server);
}int main(){WSADATA wsaData;int error;WORD wVersionRequested;wVersionRequested = WINSOCK_VERSION;error = WSAStartup(wVersionRequested, &wsaData);if (error != 0) {WSACleanup();return 0;}WSAEventServerSocket();WSACleanup();return 0;
}
客戶端
#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 網絡編程實戰》