Windows Sockets網路程式設計(3)WSAEventSelect模型開發

Doc Tsing發表於2017-07-25

摘要:WSAEventSelect模型是非阻塞的,該模型允許在一個或者多個套接字上接收以事件為基礎的網路事件通知。Windows Sockets應用程式在建立套接字後,呼叫WSAEventSelect()函式,將一個事件物件與網路事件集合關聯在一起。當網路事件發生時,應用程式以事件的形式接收網路事件通知。使用這個模型的基本思路是為感興趣的一組網路事件建立一個事件物件,再呼叫WSAEventSelect()函式將網路事件和事件物件關聯起來。當網路事件發生時,Winsock使相應的事件物件受信,在事件物件上的等待函式就會返回。WSAEventSelect模型簡單易用,也不需要視窗環境。該模型唯一的缺點是有最多等待64個事件物件的限制,當套接字連線數量增加時,就必須建立多個執行緒來處理I/O,也就是所謂的執行緒池。


目錄:

-------------------------------

- 建立TCP連結

- WSACreateEvent函式

- WSAWaitForMultipleEvents函式

- WSAEnumNetworkEvents函式

- 實踐1:WASEventSelect模型

- 實踐2:TCP Client


1.建立TCP連結

這裡不再贅述了,能來到本文的,相信基本功已經不用多講了。實在不明白的可以閱讀《Windows Sockets網路程式設計(0)TCP In Action》一文,該文詳細的敘述了TCP建立的整個過程。

SOCKET socket_listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8086);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(socket_listener, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR){
	closesocket(socket_listener);
	return;
}
listen(socket_listener, SOMAXCONN);//SOMAXCONN = 5

2.WSACreateEvent函式

在呼叫WSAEventSelect函式之間,必須要先建立事件,否則沒法監聽。Windows事件物件有兩個狀態“已觸發”和“未觸發”,而事件物件的工作模式也有兩種“人工重設”以及“自動重設”。WSACreateEvent建立的事件——原始狀態是“未觸發態”,當事件到來時系統會將其置為已觸發態,工作模式是“人工重設”。

WSAEVENT    eventArray[WSA_MAXIMUM_WAIT_EVENTS];//WSA_MAXIMUM_WAIT_EVENTS = 64
SOCKET      sockArray[WSA_MAXIMUM_WAIT_EVENTS];
int nEventTotal = 0;
WSAEVENT event = WSACreateEvent();
WSAEventSelect(socket_listener, event, FD_ACCEPT | FD_CLOSE);
eventArray[nEventTotal] = event;
sockArray[nEventTotal] = socket_listener;
++nEventTotal;

另外,需要注意的是WSAResetEvent是將事件從“已觸發”態重置為“未觸發”狀態。而WSACloseEvent主要用來釋放事件物件所佔用的資源。


3.WSAWaitForMultipleEvents函式

該函式主要用來等待一個或者所有事件變為“已觸發”態。

WSAWaitForMultipleEvents(
    _In_ DWORD cEvents,
    _In_reads_(cEvents) const WSAEVENT FAR * lphEvents,
    _In_ BOOL fWaitAll,
    _In_ DWORD dwTimeout,
    _In_ BOOL fAlertable
    );

cEvents :事件控制程式碼的數量,最多為WSA_MAXIMUM_WAIT_EVENTS = 64個;
lphEvents:事件控制程式碼存放處;
fWaitAll:有兩種狀態,TRUE時表示等待所有事件被觸發、FALSE時表示只要一個事件觸發,就繼續執行;
dwTimeout:超時時間,WSA_INFINITE表示無限等待;

fAlertable:該引數主要用於重疊I/O,此處總是設定為FALSE即可。


int nIndex = WSAWaitForMultipleEvents(nEventTotal, eventArray,FALSE,WSA_INFINITE,FALSE);


WSAWaitForMultipleEvents函式的返回值會指出——到底哪一個事件被觸發了(在設定為單一觸發的情況下)。這裡牽涉到一個巨集WSA_WAIT_EVENT_0(該巨集在winnt.h中其實是被定義為0的),一般來說where = nIndex - WSA_WAIT_EVENT_0;使用其返回值減去WSA_WAIT_EVENT_0所得的值,則表示事件陣列中,具體被觸發的那個事件下標。當然,如果fWaitAll設定為TRUE的話,根本不需要計算返回值,因為該情況下,總是全部被觸發。


4.WSAEnumNetworkEvents函式

事件發生了,也知道具體是哪個Socket被觸發了。但是,為何被觸發了?這就需要該函式來確定了。

WSAEnumNetworkEvents(
    _In_ SOCKET s,
    _In_ WSAEVENT hEventObject,
    _Out_ LPWSANETWORKEVENTS lpNetworkEvents
    );

s :被觸發的套接字控制程式碼;

hEventObject :需要被重置的Event(WSACreateEvent是“人工重置”的);

lpNetworkEvents:指向_WSANETWORKEVENTS 結構體指標,包含了到底是發生了何種網路事件,或者網路錯誤。


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

lNetworkEvents:指示發生的網路事件。當一個事件物件成為“已觸發”狀態時,可能同時發生了多個網路事件。

iErrorCode:網路事件陣列。所以事件需要使用一個陣列來存放。


WSANETWORKEVENTSevent;
WSAEnumNetworkEvents(sockArray[nIndex], eventArray[nIndex], &event);


那麼,具體要如何判斷髮生了什麼自己關注的事件呢?

if (event.lNetworkEvents &FD_ACCEPT)
{

if (event.iErrorCode[FD_ACCEPT_BIT] == 0)

{

}

}

識別符號命名是有規則的,如果需要檢查某一事件是否發生了錯誤,在對應事件末尾追加“_BIT”即可。比如FD_ACCEPT網路事件所對應的錯誤位置即為FD_ACCEPT_BIT。假若iErrorCode[FD_ACCEPT_BIT] == 0,則表示未發生錯誤。


5.實踐1:WASEventSelect模型

#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib") 
#include <stdio.h>

/*
Date      |Change
-----------------------------
2017-7-24 |WSAEventSelect模型
*/
WSAEVENT    eventArray[WSA_MAXIMUM_WAIT_EVENTS];//WSA_MAXIMUM_WAIT_EVENTS = 64
SOCKET      sockArray[WSA_MAXIMUM_WAIT_EVENTS];
int nEventTotal = 0;

void wsa_event_select()
{
	SOCKET sAccept = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8086);
	sin.sin_addr.S_un.S_addr = INADDR_ANY;
	if (bind(sAccept, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR){
		closesocket(sAccept);
		return;
	}
	listen(sAccept, SOMAXCONN);//SOMAXCONN = 5

	WSAEVENT event = WSACreateEvent();
	WSAEventSelect(sAccept, event, FD_ACCEPT | FD_CLOSE);
	eventArray[nEventTotal] = event;
	sockArray[nEventTotal] = sAccept;
	++nEventTotal;

	while (true)
	{
		int nIndex = WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
		nIndex = nIndex - WSA_WAIT_EVENT_0;

		if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
		{
			continue;
		}
		else
		{
			WSANETWORKEVENTS event;
			WSAEnumNetworkEvents(sockArray[nIndex], eventArray[nIndex], &event);
			if (event.lNetworkEvents & FD_ACCEPT)
			{
				if (event.iErrorCode[FD_ACCEPT_BIT] == 0)
				{
					if (nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
					{
						continue;
					}
					SOCKET sClient = accept(sockArray[nIndex], NULL, NULL);
					WSAEVENT event = WSACreateEvent();
					WSAEventSelect(sClient, event, FD_READ | FD_CLOSE | FD_WRITE);
					eventArray[nEventTotal] = event;
					sockArray[nEventTotal] = sClient;
					++nEventTotal;
					printf("accept.\n");
				}
			}
			else if (event.lNetworkEvents & FD_READ)         // 處理FD_READ通知訊息  
			{
				if (event.iErrorCode[FD_READ_BIT] == 0)
				{
					char data[1024];
					int ret = recv(sockArray[nIndex], data, strlen(data), 0);
					if (ret > 0)
					{
						data[ret] = '\0';
						printf("recv: %s.\n",data);
						send(sockArray[nIndex], data, strlen(data), 0);
					}
				}
			}
			else if (event.lNetworkEvents & FD_CLOSE)
			{
				if (event.iErrorCode[FD_CLOSE_BIT] == 0)
				{
					printf("close.\n");
					closesocket(sockArray[nIndex]);
					for (int j = nIndex; j < nEventTotal - 1; j++)
					{
						eventArray[j] = eventArray[j + 1];
						sockArray[j] = sockArray[j + 1];
					}
					--nEventTotal;
				}
			}
			else if (event.lNetworkEvents & FD_WRITE)
			{
				printf("send.\n");
				char* data = "qingdujun";
				send(sockArray[nIndex], data, sizeof(data)/sizeof(char), 0);
			}
		}
	}
	
}

int main(int argc, char* argv[])
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);
	wsa_event_select();
	WSACleanup();
	return 0;
}

6.實踐2:TCP Client

#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib") 
#include <stdio.h>

/*
Date      |Change
-----------------------------------------
2017-7-24 |WSAEventSelect模型,TCP測試客戶端
*/
void tcp_client()
{
	SOCKET sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8086);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (connect(sClient, (sockaddr *)&sin, sizeof(sin)) == SOCKET_ERROR){
		closesocket(sClient);
		return;
	}
	char buffer[1024] = "abcdef";
	send(sClient, buffer, strlen(buffer), 0);
	int ret = recv(sClient, buffer, sizeof(buffer), 0);
	buffer[ret] = '\0';
	printf("%s.\n",buffer);
	closesocket(sClient);
	
}

int main(int argc, char* argv[])
{
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);
	int i = 5;
	while (i--){
		tcp_client();
		Sleep(2000);
	}
	WSACleanup();
	return 0;
}


參考文獻:

[1] 孫海民. 精通Windows Sockets網路開發——基於Visual C++實現[M]. 北京:人民郵電出版社, 2008. 238-245



@qingdujun

2017-7-25 in Xi'An

相關文章