1. 程式人生 > >Socket程式設計模型之事件選擇模型

Socket程式設計模型之事件選擇模型

一 原理與關鍵函式

Winsock提供了另一個有用的非同步I/O模型。和WSAAsyncSelect模型類似的是,它也允許應用程式在一個或多個套接字上,接收以事件為基礎的網路事件通知。對於表1總結的、由WSAAsyncSelect模型採用的網路事件來說,它們均可原封不動地移植到新模型。在用新模型開發的應用程式中,也能接收和處理所有那些事件。該模型最主要的差別在於網路事件會投遞至一個事件物件控制代碼,而非投遞至一個視窗例程。

跟WSAAsyncSelect類似,但是不是通過訊息實現,而是通過事件物件。因為是基於select實現,一個執行緒也只能管理64個socket。事件選擇模型是基於訊息的。它允許程式通過Socket,接收以事件為基礎的網路事件通知。事件選擇模型相關函式主要有4個,第一個是:

WSAEVENT WSACreatEvent(void); 

它用來建立事件物件。如果函式成功,則返回值即是事件物件的控制代碼。如果函式失敗,返回WSA_INVALID_EVENT。應用程式可通過呼叫WSAGetLastError()函式獲取進一步的錯誤資訊。主要的錯誤程式碼有:
WSANOTINITIALISED       //在呼叫本API之前應成功呼叫WSAStartup()。
WSAENETDOWN             //網路子系統失效。
WSA_NOT_ENOUGH_MEMORY   //無足夠記憶體建立事件物件。

第2個是:
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject,long  lNetworkEvents); 

WSAEventSelect模型是WindowsSockets提供的一個有用非同步I/O模型。該模型允許在一個或者多個套接字上接收以事件為基礎的網路事件通知。Windows Sockets應用程式在建立套接字後,呼叫WSAEventSelect()函式,將一個事件物件與網路事件集合關聯在一起。當網路事件發生時,應用程式以事件的形式接收網路事件通知。
WSAEventSelect建立的事件擁有兩種工作狀態,以及兩種工作模式。其中,兩種工作狀態分別是“已傳信”(signaled)和“未傳信”(non signaled)。工作模式則包括“人工重設”(manual reset)和“自動重設”(auto reset)。WSAEventSelect最開始在一種未傳信的工作狀態中,並用一種人工重設模式,來建立事件控制代碼。隨著網路事件觸發了與一個套接字關聯在一起的事件物件,工作狀態便會從“未傳信”轉變成“已傳信”。由於事件物件是在一種人工重設模式中建立的,所以在完成了一個I/O請求的處理之後,我們的應用程式需要負責將工作狀態從已傳信更改為未傳信。要做到這一點,可呼叫WSAResetEvent函式,對它的定義如下:BOOL WSAResetEvent(WSAEVENT hEvent);唯一的引數是前面用WSACreateEvent函式建立的事件物件控制代碼,成功返回TRUE,失敗返回FALSE。當應用程式完成了對一個事件物件的處理後,應呼叫BOOL WSACloseEvent(WSAEVENT hEvent);函式釋放由hEvent控制代碼佔用的系統資源。成功返回TRUE,失敗返回FALSE。

第3個是:

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

只要指定事件物件中的一個或全部處於有訊號狀態,或者超時間隔到,則返回。cEvents:指出lphEvents所指陣列中事件物件控制代碼的數目。事件物件控制代碼的最大值為WSA_MAXIMUM_WAIT_EVENTS。lphEvents:指向一個事件物件控制代碼陣列的指標。fWaitAll:指定等待型別。若為真TRUE,則當lphEvents陣列中的所有事件物件同時有訊號時,函式返回。若為假FALSE,則當任意一個事件物件有訊號時函式即返回。在後一種情況下,返回值指出是哪一個事件物件造成函式返回。dwTimeout:指定超時時間間隔(以毫秒計)。當超時間隔到,函式即返回,不論fWaitAll引數所指定的條件是否滿足。如果dwTimeout為零,則函式測試指定的時間物件的狀態,並立即返回。如果dwTimeout是WSA_INFINITE,則函式的超時間隔永遠不會到。fAlertable:指定當系統將一個輸入/輸出完成例程放入佇列以供執行時,函式是否返回。若為真TRUE,則函式返回且執行完成例程。若為假FALSE,函式不返回,不執行完成例程。請注意在Win16中忽略該引數。
如果函式成功,返回值指出造成函式返回的事件物件。(這一句有問題),應該改成:如果事件陣列中有某一個事件被傳信了,函式會返回這個事件的索引值,但是這個索引值需要減去預定義值 WSA_WAIT_EVENT_0才是這個事件在事件陣列中的位置。如果函式失敗,返回值為WSA_WAIT_FAILED。可呼叫WSAGetLastError()來獲取進一步的錯誤資訊。
錯誤程式碼:
WSANOTINITIALISED     //在呼叫本API之前應成功呼叫WSAStartup()。
WSAENETDOWN           //網路子系統失效。
WSA_NOT_ENOUGH_MEMORY //無足夠記憶體完成該操作。
WSA_INVALID_HANDLE    //lphEvents陣列中的一個或多個值不是合法的事件物件控制代碼。
WSA_INVALID_PARAMETER //cEvents引數未包含合法的控制代碼數目。
第4個函式:
int WSAAPI WSAEnumNetworkEvents ( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents, LPINT lpiCount);

檢測所指定的套介面上網路事件的發生。s:標識套介面的描述字。hEventObject:(可選)控制代碼,用於標識需要復位的相應事件物件。lpNetworkEvents:一個WSANETWORKEVENTS結構的陣列,每一個元素記錄了一個網路事件和相應的錯誤程式碼。lpiCount:陣列中的元素數目。在返回時,本引數表示陣列中的實際元素數目;如果返回值是WSAENOBUFS,則表示為獲取所有網路事件所需的元素數目。
如果操作成功則返回0。否則的話,將返回SOCKET_ERROR錯誤,應用程式可通過WSAGetLastError()來獲取相應的錯誤程式碼。
錯誤程式碼:
WSANOTINITIALISED //在呼叫本API之前應成功呼叫WSAStartup()。
WSAENETDOWN       //網路子系統失效。
WSAEINVAL         //引數中有非法值。
WSAEINPROGRESS    //一個阻塞的WinSock呼叫正在進行中,或者服務提供者仍在處理一個回撥函式WSAENOBUFS 所提供的緩衝區太小。

二 完整例項與執行效果

#pragma once

#include <iserver_manager.h>
#include <WinSock2.h>
#include <common_callback.h>

class event_server_manager:
	protected iserver_manager
{
private:
	WSADATA wsa;
	int iclient_count;
	int iaddr_size;
	int iport;
	SOCKET server;
	SOCKET clients[FD_SETSIZE];
	WSAEVENT mevents[FD_SETSIZE];
	common_callback callback;
	BOOL brunning;
	
private:
	void cleanup(int index);
	
protected:
	bool accept_by_crt();
	bool accept_by_winapi();
	void receive();

public:
	void shutdown();
	void start_accept_by_crt();
	void start_accept_by_winapi();
	void start_receive();
public:
	event_server_manager();
	virtual ~event_server_manager();
};
實現檔案如下:
#include "event_server_manager.h"
#include <stdio.h>
#include <tchar.h>
#include <vector>
#include <cassert>

event_server_manager::event_server_manager()
{
	WSAStartup(MAKEWORD(2, 2), &wsa);
	iclient_count = 0;
	iport = 5150;
	iaddr_size = sizeof(SOCKADDR_IN);
	brunning = FALSE;
	callback.set_manager(this);
}

event_server_manager::~event_server_manager()
{
}

bool event_server_manager::accept_by_crt()
{
	SOCKET conn_socket;
	SOCKADDR_IN local;
	SOCKADDR_IN conn_addr;
	int iret = 0;

	server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	local.sin_family = AF_INET;
	local.sin_port = htons(iport);
	do
	{
		iret = bind(server, (struct sockaddr*)&local, iaddr_size);
		if (iret == 0)
			break;
		iport++;
		local.sin_port = htons(iport);
	} while (iret == SOCKET_ERROR);
	listen(server, 3);
	printf_s("服務已經啟動,監聽埠是:%d\n", iport);
	while (brunning)
	{
		conn_socket = accept(server, (struct sockaddr*)&conn_addr, &iaddr_size);
		if (conn_socket == INVALID_SOCKET)
		{
			printf_s("拒絕一個連線。\n");
			continue;
		}
		printf_s("新連線%s:%d。\n", inet_ntoa(conn_addr.sin_addr), ntohs(conn_addr.sin_port));
		clients[iclient_count] = conn_socket;
		mevents[iclient_count] = WSACreateEvent();
		WSAEventSelect(conn_socket, mevents[iclient_count], FD_ALL_EVENTS);
		iclient_count++;
	}
	return true;
}

bool event_server_manager::accept_by_winapi()
{
	return true;
}

void event_server_manager::receive()
{
	DWORD iret = 0;
	int index = 0;
	WSANETWORKEVENTS nevent;
	char message[1024] = { 0 };

	while (brunning)
	{
		iret = WSAWaitForMultipleEvents(iclient_count, mevents, FALSE, 1000, FALSE);
		if (iret == WSA_WAIT_FAILED || iret == WSA_WAIT_TIMEOUT)
			continue;
		index = iret - WSA_WAIT_EVENT_0;
		WSAEnumNetworkEvents(clients[index], mevents[index], &nevent);
		if (nevent.lNetworkEvents&FD_READ)
		{
			iret = recv(clients[index], message, 1024, 0);
			if (iret == 0 || (iret == SOCKET_ERROR&&WSAGetLastError() == WSAECONNRESET))
				cleanup(index);
			else
			{
				message[iret] = 0;
				send(clients[index], message, iret, 0);
			}
		}
		if (nevent.lNetworkEvents&FD_CLOSE)
		{
			cleanup(index);
		}
	}
}

void event_server_manager::cleanup(int index)
{
	SOCKADDR_IN client;

	assert(index < iclient_count);
	getpeername(clients[index], (struct sockaddr*)&client, &iaddr_size);
	printf_s("客戶端%s:%d斷開連線。\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
	closesocket(clients[index]);
	WSACloseEvent(mevents[index]);
	if (index < iclient_count - 1)
	{
		clients[index] = clients[iclient_count - 1];
		mevents[index] = mevents[iclient_count - 1];
	}
	clients[iclient_count - 1] = 0;
	mevents[iclient_count - 1] = INVALID_HANDLE_VALUE;
	iclient_count--;
}

void event_server_manager::shutdown()
{
	brunning = FALSE;
	callback.shutdown();
	for (int i = 0; i < iclient_count; i++)
	{
		closesocket(clients[i]);
		WSACloseEvent(mevents[i]);
	}
	closesocket(server);
	WSACleanup();
}

void event_server_manager::start_accept_by_crt()
{
	brunning = TRUE;
	callback.start_accept_by_crt();
}

void event_server_manager::start_accept_by_winapi()
{
	brunning = TRUE;
	callback.start_accept_by_winapi();
}

void event_server_manager::start_receive()
{
	brunning = TRUE;
	callback.start_receive();
}

int main(int argc, char** argv)
{
	event_server_manager esm;

	esm.start_accept_by_crt();
	esm.start_receive();
	printf_s("按任意鍵關閉服務端並退出程式。\n");
	system("pause");
	esm.shutdown();
	return 0;
}

執行效果如下: