一、什么是事件選擇模型
事件選擇(WSAEventSelect)模型是另一個有用的異步 I/O 模型。和 WSAAsyncSelect 模型類似的是,它也允許應用程序在一個或多個套接字上,接收以事件為基礎的網絡事件通知,最主要的差別在於網絡事件會投遞至一個事件對象句柄,而非投遞到一個窗口例程。
每一個socket都配備一個event,開發者可以為event注冊對應的網絡事件,當
有事件來的時候,對應socket的event就會變成有信號狀態。
二、事件選擇模型API函數
事件通知模型要求我們的應用程序針對使用的每一個套接字,首先創建一個事件對象。創建方法是調用 WSACreateEvent 函數,它的定義如下:
WSAEVENT WSACreateEvent(void);
WSACreateEvent 函數的返回值很簡單,就是一個創建好的事件對象句柄,接下來必須將其與某個套接字關聯在一起,同時注冊自己感興趣的網絡事件類型
(FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE等),方法是調用 WSAEventSelect 函數,其定義如下:
int WSAEventSelect( __in SOCKET s, //代表感興趣的套接字 __in WSAEVENT hEventObject, //指定要與套接字關聯在一起的事件對象,即用 WSACreateEvent 創建的那一個 __in long lNetworkEvents //對應一個“位掩碼”,用於指定應用程序感興趣的各種網絡事件類型的一個組合。 );
其中參數 lNetworkEvents可以用以下數值進行OR操作
FD_READ 應用程序想要接收有關是否可讀的通知,以便讀入數據
FD_WRITE 應用程序想要接收有關是否可寫的通知,以便寫入數據
FD_ACCEPT 應用程序想接收與進入連接有關的通知
FD_CONNECT 應用程序想接收與一次連接完成的通知
FD_CLOSE 應用程序想接收與套接字關閉的通知
WSACreateEvent 創建的事件有兩種工作狀態,以及兩種工作模式。工作狀態分別是“已傳信”(signaled)和“未傳信”(nonsignaled)。工作模式則包括“人工重設”(manual reset)和“自動重設”(auto reset)。WSACreateEvent 開始是在一種未傳信的工作狀態,並用一種人工重設模式,來創建事件句柄。隨着網絡事件觸發了與一個套接字關聯在一起的事件對象,工作狀態便會從“未傳信”轉變成“已傳信”。由於事件對象是在一種人工重設模式中創建的,所以在完成了一個 I/O 請求的處理之后,我們的應用程序需要負責將工作狀態從已傳信更改為未傳信。要做到這一點,可調用 WSAResetEvent 函數,對它的定義如下:
BOOL WSAResetEvent(
__in WSAEVENT hEvent //事件句柄;
);
該函數調用是成功還是失敗,會分別返回TRUE或FALSE。
應用程序完成了對一個事件對象的處理后,便應調用WSACloseEvent函數,釋放由事件句柄使用的系統資源。對 WSACloseEvent 函數的定義如下:
BOOL WSACloseEvent(
__in WSAEVENT hEvent //事件句柄;
);
該函數調用是成功還是失敗,會分別返回TRUE或FALSE。
一個套接字同一個事件對象句柄關聯在一起后,應用程序便可開始I/O處理;方法是等待網絡事件觸發事件對象句柄的工作狀態。WSAWaitForMultipleEvents 函數的設
計宗旨便是用來等待一個或多個事件對象句柄,並在事先指定的一個或所有句柄進入“已傳信”狀態后,或在超過了一個規定的時間周期后,立即返回。下面是
WSAWaitForMultipleEvents 函數的定義:
DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in BOOL fAlertable );
cEvents 和 lphEvents 參數定義了由 WSAEVENT 對象構成的一個數組。在這個數組中,cEvents指定的是事件對象的數量,而lphEvents對應的是一個指針,用於
直接引用該數組。要注意的是,WSAWaitForMultipleEvents 只能支持由 WSA_MAXIMUM_WAIT_EVENTS 對象規定的一個最大值,在此定義成64個。因此,針對
發出 WSAWaitForMultipleEvents 調用的每個線程,該 I/O 模型一次最多都只能支持64個套接字。假如想讓這個模型同時管理不止64個套接字,必須創建額外的工作
者線程,以便等待更多的事件對象。
fWaitAll 參數指定了 WSAWaitForMultipleEvents 如何等待在事件數組中的對象。若設為TRUE,那么只有等 lphEvents 數組內包含的所有事件對象都已進入“已
傳信”狀態,函數才會返回;但若設為FALSE,任何一個事件對象進入“已傳信”狀態,函數就會返回。就后一種情況來說,返回值指出了到底是哪個事件對象造成了函數的
返回。通常,應用程序應將該參數設為 FALSE,一次只為一個套接字事件提供服務。
dwTimeout參數規定了 WSAWaitForMultipleEvents 最多可等待一個網絡事件發生有多長時間,以毫秒為單位,這是一項“超時”設定。超過規定的時間,函數就會
立即返回,即使由 fWaitAll 參數規定的條件尚未滿足也如此。考慮到它對性能造成的影響,應盡量避免將超時值設為0。假如沒有等待處理的事件,
WSAWaitForMultipleEvents 便會返回 WSA_WAIT_TIMEOUT。如 dwTimeout 設為 WSAINFINITE(永遠等待),那么只有在一個網絡事件傳信了一個事件對象
后,函數才會返回。
三、代碼示例
客戶端服務器收發數據進行通信:
server:
#include <Winsock2.h>
#include <windows.h>
#include "TcpSocket.h"
#include "CLock.h"
#include <vector>
using namespace std;
CLock g_lock;
bool HandleData(SOCKET sockClient)
{
// 5) 收發數據
char aryBuff[MAXWORD] = { 0 };
int nRet = recv(sockClient, aryBuff, sizeof(aryBuff), 0);
if (nRet == 0 || nRet == SOCKET_ERROR)
{
printf("接受數據失敗 \n");
return false;
}
printf("收到數據: %s \n", aryBuff);
char szBuff[] = { "recv OK \r\n" };
nRet = send(sockClient, szBuff, sizeof(szBuff), 0);
if (nRet == SOCKET_ERROR)
{
printf("數據發送失敗 \n");
return false;
}
return true;
}
//線程, 用來處理客戶端, 和客戶端進行數據的收發
DWORD WINAPI HandleClientsThread(LPVOID pParam)
{
//vector<SOCKET>& vctClients =*(vector<SOCKET>*)pParam;
vector<pair<SOCKET, WSAEVENT>>& vctClients = *(vector<pair<SOCKET, WSAEVENT>>*)pParam;
while (TRUE)
{
/*
fd_set fdRead;
FD_ZERO(&fdRead); //初始化
*/
WSAEVENT aryEvents[WSA_MAXIMUM_WAIT_EVENTS];
int nCount = 0;
//把所有客戶端加入數組
g_lock.Lock();
for (auto& pairSockEvent : vctClients)
{
//FD_SET(sock, &fdRead);
aryEvents[nCount++] = pairSockEvent.second;
}
g_lock.UnLock();
/*
timeval tv = { 1, };
int nRet = select(fdRead.fd_count,
&fdRead,
NULL,
NULL,
&tv);
if (nRet == 0 || nRet==SOCKET_ERROR)
{
continue;
}
*/
//檢測指定的socket
int nRet = WSAWaitForMultipleEvents(nCount, aryEvents, FALSE, 1000, FALSE);
if (nRet == WSA_WAIT_TIMEOUT) //超時繼續等待
{
continue;
}
//處理數據
g_lock.Lock();
for (auto itr = vctClients.begin(); itr != vctClients.end(); itr++)
{
//判斷sock是否可以讀數據
//if (FD_ISSET(*itr, &fdRead))
//判斷socket是否是可以讀數據了
if (itr->second == aryEvents[nRet])
{
WSANETWORKEVENTS workevent;
WSAEnumNetworkEvents(itr->first, itr->second, &workevent);
if (workevent.lNetworkEvents & FD_READ)
{
//if (itr->second == aryEvents[nRet]);
if (!HandleData(itr->first))
{
//連接斷開
vctClients.erase(itr);
break;
}
}
else if (workevent.lNetworkEvents & FD_CLOSE)
{
vctClients.erase(itr);
}
}
}
g_lock.UnLock();
}
return 0;
}
client
int main()
{
//1.創建socket
SOCKET sockServer = socket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP
);
//2.綁定端口
sockaddr_in siServer;
siServer.sin_family = AF_INET;
siServer.sin_port = htons(9527);
siServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int nRet = bind(sockServer, (sockaddr*)&siServer, sizeof(siServer));
if (nRet == SOCKET_ERROR)
{
printf("端口綁定失敗\n");
return 0;
}
//3.監聽
nRet = listen(sockServer, SOMAXCONN);//監聽最大值
if (nRet == SOCKET_ERROR)
{
printf("監聽失敗\n");
return 0;
}
//創建線程,檢測socket是否有數據可讀並處理
//vector<SOCKET> vctClients;
vector<pair<SOCKET, WSAEVENT>>vctClients;
HANDLE hTread = CreateThread(NULL, 0, HandleClientsThread, (LPVOID)&vctClients, 0, NULL);
CloseHandle(hTread);
while (true)
{
// 4) 接受連接
sockaddr_in siClient;
int nSize = sizeof(siClient);
printf("客戶端已就緒,等待連接");
SOCKET sockClient = accept(sockServer, (sockaddr*)&siClient, &nSize);
if (sockClient == SOCKET_ERROR)
{
printf("接受連接失敗 \r\n");
return 0;
}
printf("IP:%s port:%d 連接到服務器. \r\n",
inet_ntoa(siClient.sin_addr),
ntohs(siClient.sin_port));
g_lock.Lock();
WSAEVENT hEvent = WSACreateEvent();
WSAEventSelect(sockClient, hEvent, FD_READ | FD_CLOSE);
vctClients.push_back(make_pair(sockClient, hEvent));
g_lock.UnLock();
}
return 0;
}
