Windows WSAEventSelect 網路通信模型

WSAEventSelect 網路通信模型是 Windows 系統上常用的一種非同步 socket 通信模型,下面來詳細介紹下其用法。

WSAEventSelect 用於伺服器端

我們先從伺服器端來看這個模型,在 Windows 系統上正常的一個伺服器端 socket 通信流程是先初始化套接字型檔,然後創建偵聽 socket,接著綁定 ip 地址和埠,再調用 listen 函數開啟偵聽。代碼如下:

//1. 初始化套接字型檔
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
int nError = WSAStartup(wVersionRequested, &wsaData);
if (nError != 0)
return -1;

if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup();
return -1;
}

//2. 創建用於監聽的套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);

//3. 綁定套接字
if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
closesocket(sockSrv);
WSACleanup();
return -1;
}

//4. 將套接字設為監聽模式,準備接受客戶請求
if (listen(sockSrv, SOMAXCONN) == SOCKET_ERROR)
{
closesocket(sockSrv);
WSACleanup();
return -1;
}

正常的流程下接著是等待客戶端連接,然後調用 accept 接受客戶端連接。在這裡,我們使用 WSAEventSelect 函數給偵聽 socket 設置需要關注的事件。WSAEventSelect 的函數如下:

int WSAAPI WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject,
long lNetworkEvents
);

  • 參數 s 是需要操作的 socket 句柄;
  • 參數 hEventObject 是需要與 socket 關聯的內核事件對象,可以使用 WSACreateEvent 函數創建:

WSAEVENT WSAAPI WSACreateEvent();

WSAEVENT 類型本質上就是使用 CreateEvent 創建的 Event 對象:

#define WSAEVENT HANDLE

  • 參數 lNetworkEvents 是 socket 上需要關注的事件,常用的事件類型有:
  • 返回值WSAEventSelect 函數調用成功返回 0,調用失敗返回 SOCKET_ERROR(-1)。

由於我們這裡是偵聽 socket,所以我們關注的事件是 FD_ACCEPT,代碼如下:

WSAEVENT hListenEvent = WSACreateEvent();
if (WSAEventSelect(sockSrv, hListenEvent, FD_ACCEPT) == SOCKET_ERROR)
{
WSACloseEvent(hListenEvent);
closesocket(sockSrv);
WSACleanup();
return -1;
}

當 socket 上有我們關注的事件時,操作系統會讓 hListenEvent 對象受信,所以接著我們使用 WSAWaitForMultipleEvents 函數去等待 hListenEvent 是否有信號,WSAWaitForMultipleEvents 簽名如下:

DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT* lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);

這個函數的使用方法和 WaitForMultipleObjects 一模一樣,我們在第三章介紹過了,這裡不再介紹。

調用 WSAWaitForMultipleEvents 示例代碼如下:

WSAEVENT hEvents[1];
hEvents[0] = hListenEvent;
DWORD dwResult = WSAWaitForMultipleEvents(1, hEvents, FALSE, WSA_INFINITE, FALSE);

DWORD dwIndex = dwResult - WSA_WAIT_EVENT_0;
for (DWORD i = 0; i < dwIndex; ++i)
{
//通過dwIndex編號找到hEvents數組中的WSAEvent對象,進而找到對應的socket
}

通過 dwIndex 編號找到 hEvents 數組中的 WSAEvent 對象,進而找到對應的 socket,然後對這個 socket 調用 WSAEnumNetworkEvents 函數來獲取該 socket 上的事件類型,WSAEnumNetworkEvents 函數簽名如下:

int WSAAPI WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);

參數 lpNetworkEvents 是一個輸出參數,其類型是 WSANETWORKEVENTS 結構體指針,其定義如下:

typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

在調用 WSAEnumNetworkEvents 後我們就能通過 lNetworkEvents 類型得到對應的 socket 的事件類型,通過 iErrorCode 欄位數組中的某一位確定該類型的事件是否有錯誤(0 值表示沒有錯誤,非 0 值表示存在錯誤),與 FD_XXX 相對應,iErrorCode 每個下標都有確定的含義,下標值都被定義成了相應的宏,常見的有:

/*
* WinSock 2 extension -- bit values and indices for FD_XXX network events
*/
#define FD_READ_BIT 0
#define FD_READ (1 << FD_READ_BIT)

#define FD_WRITE_BIT 1
#define FD_WRITE (1 << FD_WRITE_BIT)

#define FD_OOB_BIT 2
#define FD_OOB (1 << FD_OOB_BIT)

#define FD_ACCEPT_BIT 3
#define FD_ACCEPT (1 << FD_ACCEPT_BIT)

#define FD_CONNECT_BIT 4
#define FD_CONNECT (1 << FD_CONNECT_BIT)

#define FD_CLOSE_BIT 5
#define FD_CLOSE (1 << FD_CLOSE_BIT)

#define FD_QOS_BIT 6
#define FD_QOS (1 << FD_QOS_BIT)

#define FD_GROUP_QOS_BIT 7
#define FD_GROUP_QOS (1 << FD_GROUP_QOS_BIT)

#define FD_ROUTING_INTERFACE_CHANGE_BIT 8
#define FD_ROUTING_INTERFACE_CHANGE (1 << FD_ROUTING_INTERFACE_CHANGE_BIT)

#define FD_ADDRESS_LIST_CHANGE_BIT 9
#define FD_ADDRESS_LIST_CHANGE (1 << FD_ADDRESS_LIST_CHANGE_BIT)

#define FD_MAX_EVENTS 10
#define FD_ALL_EVENTS ((1 << FD_MAX_EVENTS) - 1)

WSAEnumNetworkEvents 函數使用示例代碼如下:

WSANETWORKEVENTS triggeredEvents;
if (WSAEnumNetworkEvents(sockSrv, hEvents[dwIndex], &triggeredEvents) != SOCKET_ERROR)
{
if (triggeredEvents.lNetworkEvents & FD_ACCEPT)
{
// 0 值表示無錯誤
if (triggeredEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
{
//TODO:在這裡可以調用accept函數處理接受連接事件。
}
}
}

上述代碼第 9 行我們可以調用 accept 函數接受新連接,然後將新產生的 clientsocket 設置監聽 FD_READ 和 FD_CLOSE 等事件。完整的代碼如下所示:

/**
* WSAEventSelect 模型演示
* zhangyl 2019.03.16
*/
#include "stdafx.h"
#include <winsock2.h>
#include <stdio.h>
#include <vector>

#pragma comment(lib, "ws2_32.lib")

int main(int argc, _TCHAR* argv[])
{
//1. 初始化套接字型檔
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
int nError = WSAStartup(wVersionRequested, &wsaData);
if (nError != 0)
return -1;

if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup();
return -1;
}

//2. 創建用於監聽的套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);

//3. 綁定套接字
if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
closesocket(sockSrv);
WSACleanup();
return -1;
}

//4. 將套接字設為監聽模式,準備接受客戶請求
if (listen(sockSrv, SOMAXCONN) == SOCKET_ERROR)
{
closesocket(sockSrv);
WSACleanup();
return -1;
}

WSAEVENT hListenEvent = WSACreateEvent();
if (WSAEventSelect(sockSrv, hListenEvent, FD_ACCEPT) == SOCKET_ERROR)
{
WSACloseEvent(hListenEvent);
closesocket(sockSrv);
WSACleanup();
return -1;
}

WSAEVENT* pEvents = new WSAEVENT[1];
pEvents[0] = hListenEvent;
SOCKET* pSockets = new SOCKET[1];
pSockets[0] = sockSrv;
DWORD dwCount = 1;
bool bNeedToMove;

while (true)
{
bNeedToMove = false;
DWORD dwResult = WSAWaitForMultipleEvents(dwCount, pEvents, FALSE, WSA_INFINITE, FALSE);
if (dwResult == WSA_WAIT_FAILED)
continue;

DWORD dwIndex = dwResult - WSA_WAIT_EVENT_0;
for (DWORD i = 0; i <= dwIndex; ++i)
{
//通過dwIndex編號找到hEvents數組中的WSAEvent對象,進而找到對應的socket
WSANETWORKEVENTS triggeredEvents;
if (WSAEnumNetworkEvents(pSockets[i], pEvents[i], &triggeredEvents) == SOCKET_ERROR)
continue;

if (triggeredEvents.lNetworkEvents & FD_ACCEPT)
{
if (triggeredEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;

//調用accept函數處理接受連接事件;
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
//等待客戶請求到來
SOCKET hSockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
if (hSockClient != SOCKET_ERROR)
{
//監聽客戶端socket的可讀和關閉事件
WSAEVENT hClientEvent = WSACreateEvent();
if (WSAEventSelect(hSockClient, hClientEvent, FD_READ | FD_CLOSE) == SOCKET_ERROR)
{
WSACloseEvent(hClientEvent);
closesocket(hSockClient);
continue;
}

WSAEVENT* pEvents2 = new WSAEVENT[dwCount + 1];
SOCKET* pSockets2 = new SOCKET[dwCount + 1];
memcpy(pEvents2, pEvents, dwCount * sizeof(WSAEVENT));
pEvents2[dwCount] = hClientEvent;
memcpy(pSockets2, pSockets, dwCount * sizeof(SOCKET));
pSockets2[dwCount] = hSockClient;
delete[] pEvents;
delete[] pSockets;
pEvents = pEvents2;
pSockets = pSockets2;

dwCount++;

printf("a client connected, socket: %d, current: %d
", (int)hSockClient, dwCount - 1);
}
}
else if (triggeredEvents.lNetworkEvents & FD_READ)
{
if (triggeredEvents.iErrorCode[FD_READ_BIT] != 0)
continue;

char szBuf[64] = { 0 };
int nRet = recv(pSockets[i], szBuf, 64, 0);
if (nRet > 0)
{
printf("recv data: %s, client: %d
", szBuf, pSockets[i]);
}
}
else if (triggeredEvents.lNetworkEvents & FD_CLOSE)
{
//此處不要判斷
//if (triggeredEvents.iErrorCode[FD_READ_BIT] != 0)
// continue;

printf("a client disconnected, socket: %d, current: %d
", (int)pSockets[i], dwCount - 2);

WSACloseEvent(pEvents[i]);
closesocket(pSockets[i]);

//標記為無效,循環結束後統一移除
pSockets[i] = INVALID_SOCKET;

bNeedToMove = true;
}

}// end for-loop

if (bNeedToMove)
{
//移除無效的事件
std::vector<SOCKET> vValidSockets;
std::vector<HANDLE> vValidEvents;
for (size_t i = 0; i < dwCount; ++i)
{
if (pSockets[i] != INVALID_SOCKET)
{
vValidSockets.push_back(pSockets[i]);
vValidEvents.push_back(pEvents[i]);
}
}

size_t validSize = vValidSockets.size();
if (validSize > 0)
{
WSAEVENT* pEvents2 = new WSAEVENT[validSize];
SOCKET* pSockets2 = new SOCKET[validSize];
memcpy(pEvents2, &vValidEvents[0], validSize * sizeof(WSAEVENT));
memcpy(pSockets2, &vValidSockets[0], validSize * sizeof(SOCKET));
delete[] pEvents;
delete[] pSockets;
pEvents = pEvents2;
pSockets = pSockets2;

dwCount = validSize;
}
}

}// end while-loop

closesocket(sockSrv);

WSACleanup();

return 0;
}

在 Visual Studio 2013 中編譯該程序並運行,然後使用 Linux nc 命令模擬幾個客戶端連接該程序,效果如下所示:


本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能伺服器開發 QQ 群一起交流: 578019391 。


推薦閱讀:
相关文章