windows socket網路程式設計--事件選擇模型

2022-11-11 21:00:59


編寫前說明

團隊成員
汪月月 組長
駱念念 組員
曹卉潼 組員
唐宇悅 組員

事件選擇模型概述

Winsock提供了另一種有用的非同步事件通知I/O模型——WSAEventSelect模型。這個模型與WSAAsyncSelect模型類似,允許應用程式在一個或者多個通訊端上接收基於事件的網路通知。它與 WSAAsyncSelect模型類似是因為它也接收FDXXX型別的網路事件,不過並不是依靠Windows的訊息驅動機制,而是經由事件物件控制程式碼通知

API詳解

WSAEVENT WSAAPI WSACreateEvent();

返回值
如果未發生錯誤, WSACreateEvent 將返回事件物件的控制程式碼。 否則,返回值WSA_INVALID_EVENT。
作用
建立新的事件物件

int WSAAPI WSAEventSelect(
  SOCKET   s,            //標識通訊端的描述符。
  WSAEVENT hEventObject, //標識要與指定FD_XXX網路事件集關聯的事件物件的控制程式碼。
  long     lNetworkEvents//一個位掩碼,指定應用程式感興趣的FD_XXX網路事件的組合。
);

返回值
如果應用程式的網路事件的規範和關聯的事件物件成功,則返回值為零。 否則,返回值SOCKET_ERROR。
作用
給事件綁上socket與操作碼,並投遞給作業系統,應用程式便可以在事件上等待了。

事件型別 含義
FD_READ 應用程式想接收是否有可讀的通知
FD_WRITE 應用程式想接收是否有可寫的通知
FD_OOB 應用程式想接收是否有OOB資料抵達通知
FD_ACCEPT 應用程式想接收與傳入連線有關的通知
FD_CONNECT 應用程式想接收一個已完成連線的通知或者一個多點join操作的通知
FD_CLOSE 應用程式想接收與通訊端關閉有關的通知
DWORD WSAAPI WSAWaitForMultipleEvents(
  DWORD          cEvents,
  const WSAEVENT *lphEvents,
  BOOL           fWaitAll,
  DWORD          dwTimeout,
  BOOL           fAlertable
);

cEvents
lphEvents 指向的陣列中的事件物件控制程式碼數。 事件物件控制程式碼的最大數目 是WSA_MAXIMUM_WAIT_EVENTS。 必須指定一個或多個事件。

lphEvents
指向事件物件控制程式碼陣列的指標。 陣列可以包含不同型別的物件的控制程式碼。 如果 fWaitAll 引數設定為 TRUE,則它可能不包含同一控制程式碼的多個副本。 如果在等待仍在掛起時關閉其中一個控制程式碼,則未定義 WSAWaitForMultipleEvents 的行為。

fWaitAll
一個指定等待型別的值。 如果為 TRUE,則當 發出 lphEvents 陣列中所有物件的狀態時,函數將返回。 如果為 FALSE,則函數在發出任何事件物件的訊號時返回。 在後一種情況下,返回值減 去WSA_WAIT_EVENT_0 指示導致函數返回其狀態的事件物件的索引。 如果在呼叫期間發出了多個事件物件的訊號,則這是訊號事件物件的陣列索引,其索引值為所有訊號事件物件的最小索引值。

dwTimeout
超時間隔(以毫秒為單位)。 WSAWaitForMultipleEvents 如果超時間隔過期,即使 不滿足 fWaitAll 引數指定的條件,也會返回。 如果 dwTimeout 引數為零, WSAWaitForMultipleEvents 將測試指定事件物件的狀態並立即返回。 如果 dwTimeoutWSA_INFINITE, WSAWaitForMultipleEvents 將永遠等待;也就是說,超時間隔永遠不會過期。

fAlertable
一個值,該值指定執行緒是否處於可警報的等待狀態,以便系統可以執行I/O完成例程。 如果為TRUE,則執行緒處於可警報的等待狀態,當系統執行 I/O 完成例程時, WSAWaitForMultipleEvents 可以返回。 在這種情況下,將返回 WSA_WAIT_IO_COMPLETION ,並且等待的事件尚未發出訊號。 應用程式必須再次呼叫 WSAWaitForMultipleEvents 函數。 如果為FALSE,則執行緒不會處於可警報的等待狀態,並且不會執行 I/O 完成例程。

int WSAAPI WSAEnumNetworkEvents(
  SOCKET             s,              //標識通訊端
  WSAEVENT           hEventObject,   //用於標識要重置的關聯事件物件的可選控制程式碼
  LPWSANETWORKEVENTS lpNetworkEvents //指向 WSANETWORKEVENTS 結構的指標,該結構填充了發生的網路事件記錄和任何關聯的錯誤程式碼
);

返回值
如果操作成功,則返回值為零。 否則,返回值SOCKET_ERROR
作用
列舉出與事件物件相關聯的通訊端發生了哪些訊號,結果放在WSANETWORKEVENTS結構體中

工作原理

流程大致是這樣:

  1. 定義一個socket陣列和event陣列
  2. 每一個socket操作關聯一個event物件
  3. 呼叫WSAWaitForMultipleEvents函數等待事件的觸發
  4. 呼叫WSAEnumNetworkEvents函數檢視是哪個一個事件,根據事件找到相應的socket,然後進行相應的處理:比如資料顯示等,同時,記得要將那個event重置為無訊號狀態。
  5. 迴圈步驟3和4,直到伺服器退出。
    流程圖

程式碼實現

伺服器端

UINT  CMFCWSAEventDlg::ThreadProc(LPVOID lparam) {
	// TODO: 在此新增控制元件通知處理程式程式碼
	CMFCWSAEventDlg* p = (CMFCWSAEventDlg*)lparam;
	SocketInit socketInit;
	SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (socketServer == INVALID_SOCKET) {
		AfxMessageBox(_T("通訊端建立失敗"));
		closesocket(socketServer);
		WSACleanup();
	}
	sockaddr_in sock;
	sock.sin_family = AF_INET;
	sock.sin_port = htons(5678);
	sock.sin_addr.S_un.S_addr = INADDR_ANY;
	int n = sizeof(sock);
	if (bind(socketServer, (sockaddr*)&sock, sizeof(sock)) == SOCKET_ERROR) {
		AfxMessageBox(_T("監聽失敗"));
		closesocket(socketServer);
		WSACleanup();

	}
	if (listen(socketServer, SOMAXCONN) == SOCKET_ERROR) {
		AfxMessageBox(_T("監聽失敗"));
		closesocket(socketServer);
		WSACleanup();
	}
	p->showText.SetWindowText("開始監聽\r\n");
	
	// 建立事件物件,並關聯到新的套節字
	WSAEVENT event = ::WSACreateEvent();
	::WSAEventSelect(socketServer, event, FD_ACCEPT | FD_CLOSE);
	// 新增到表中
	p->eventArray[p->nEventTotal] = event;
	p->sockArray[p->nEventTotal] = socketServer;
	p->nEventTotal++;
	CString str;
	sockaddr_in addrRemote;
	
	while (1){

		// 在所有事件物件上等待
		int nIndex = ::WSAWaitForMultipleEvents(p->nEventTotal, p->eventArray, FALSE, WSA_INFINITE, FALSE);
		// 對每個事件呼叫WSAWaitForMultipleEvents函數,以便確定它的狀態
		nIndex = nIndex - WSA_WAIT_EVENT_0;

		for (int i = nIndex; i < p->nEventTotal; i++)
		{
			nIndex = ::WSAWaitForMultipleEvents(1, &p->eventArray[i], TRUE, 1000, FALSE);
			if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
			{
				continue;
			}
			else
			{
				// 獲取到來的通知訊息,WSAEnumNetworkEvents函數會自動重置受信事件
				WSANETWORKEVENTS event;
				
				::WSAEnumNetworkEvents(p->sockArray[i], p->eventArray[i], &event);
				if (event.lNetworkEvents & FD_ACCEPT)                // 處理FD_ACCEPT通知訊息
				{
					if (event.iErrorCode[FD_ACCEPT_BIT] == 0)
					{
						if (p->nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
						{
							p->showText.SetSel(-1);
							p->showText.ReplaceSel("時間太長");
							continue;
						}
						int nAddrLen = sizeof(addrRemote);
						SOCKET sNew = ::accept(p->sockArray[i], (SOCKADDR*)&addrRemote, &nAddrLen);
						//MessageBox("已連線");
						int nLen = p->showText.GetWindowTextLengthA();
						//p->showText.SetWindowText()
						
						str.Format("%s建立連線\r\n", ::inet_ntoa(addrRemote.sin_addr));
						p->showText.SetSel(-1);
						p->showText.ReplaceSel(str);
						WSAEVENT event = ::WSACreateEvent();
						::WSAEventSelect(sNew, event, FD_READ | FD_CLOSE | FD_WRITE);
						// 新增到表中
						p->eventArray[p->nEventTotal] = event;
						p->sockArray[p->nEventTotal] = sNew;
						p->nEventTotal++;
					}
				}
				else if (event.lNetworkEvents & FD_READ)         // 處理FD_READ通知訊息
				{
					if (event.iErrorCode[FD_READ_BIT] == 0)
					{
						//char szText[256];
						char szText[1024] = { 0 };
						//memset(szText, 0, sizeof(szText));
						int nlen = strlen(szText);
						int nRecv = ::recv(p->sockArray[i], szText,1024, 0);
						
						//AfxMessageBox(nRecv);
						if (nRecv > 0)
						{
							szText[nRecv] = '\0';
							str.Format("%s發來了一條訊息:%s\r\n", ::inet_ntoa(addrRemote.sin_addr), szText);
							p->showText.SetSel(-1);
							p->showText.ReplaceSel(str);
							//szText[0] = '\0';

							// 向用戶端傳送資料
							char *sendText =  getallprime(1000);
							if (::send(p->sockArray[i], sendText, strlen(sendText), 0) > 0)
							{
								p->showText.SetSel(-1);
								p->showText.ReplaceSel("已傳送結果\r\n");
							}
						}
					}
				}
				else if (event.lNetworkEvents & FD_CLOSE)        // 處理FD_CLOSE通知訊息
				{
					if (event.iErrorCode[FD_CLOSE_BIT] == 0)
					{
						::closesocket(p->sockArray[i]);
						for (int j = i; j < p->nEventTotal - 1; j++)
						{
							p->eventArray[j] = p->eventArray[j + 1];
							p->sockArray[j] = p->sockArray[j + 1];
						}
						p->nEventTotal--;
					}
					p->showText.SetSel(-1);
					p->showText.ReplaceSel("關閉連線\r\n");
				}
			}
		}
	}

}

使用者端

void CMFCWSAEventClientDlg::OnBnClickedButton1()
{
	// TODO: 在此新增控制元件通知處理程式程式碼
	AfxBeginThread(ThreadProc, this);
}

UINT  CMFCWSAEventClientDlg::ThreadProc(LPVOID lparam) {
	CMFCWSAEventClientDlg* p = (CMFCWSAEventClientDlg*)lparam;
	// 建立套節字
	SocketInit socketInit;
	p->s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (p->s == INVALID_SOCKET){
		AfxMessageBox(" Failed socket()");
		closesocket(p->s);
		WSACleanup();
	}

	// 也可以在這裡呼叫bind函數繫結一個本地地址
	// 否則系統將會自動安排

	BYTE nFild[4];
	p->ipAddr.GetAddress(nFild[0], nFild[1], nFild[2], nFild[3]);
	CString serverIP, serverPort;
	serverIP.Format("%d.%d.%d.%d", nFild[0], nFild[1], nFild[2], nFild[3]);
	p->port.GetWindowTextA(serverPort);
	sockaddr_in servAddr;
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(atoi(serverPort));
	servAddr.sin_addr.S_un.S_addr = inet_addr(serverIP);

	if (::connect(p->s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
	{
		AfxMessageBox(" Failed connect() ");
		closesocket(p->s);
		WSACleanup();
	}
	CString str,szText;
	str.Format("與伺服器%s建立連線\r\n", serverIP);
	p->showText.SetSel(-1);
	p->showText.ReplaceSel(str);
	while (1) {
		//傳送資料
		p->OnBnClickedButton2();
		// 接收資料
		char buff[1024];
		int nRecv = ::recv(p->s, buff, 1024, 0);
		if (nRecv > 0){
			buff[nRecv] = '\0';
			str.Format("伺服器端:%s\r\n", buff);
			p->showText.SetSel(-1);
			p->showText.ReplaceSel(str);
		}
		p->sendText.SetWindowText("");
	}
	return 0;
}
void CMFCWSAEventClientDlg::OnBnClickedButton2()
{
	CString str,szText;
	sendText.GetWindowTextA(szText);
	// TODO: 在此新增控制元件通知處理程式程式碼
	if (szText != "") {
		if (::send(s, szText, strlen(szText), 0) > 0){
			str.Format("使用者端:%s\r\n", szText);
			showText.SetSel(-1);
			showText.ReplaceSel(str);
		}
	}
	
}