TCP/IP網絡編程_基於Windows的編程_第21章異步通知I/O模型

在這裏插入圖片描述

21.1 理解異步通知I/O模型

各位應該還記得介紹過的 select 函數, 它是實現併發服務器端的方法之一. 本章內容可以理解爲 select 模型的改進方式.

理解同步和異步

首先解析 “異步(Asynchronous)的含義”. 異步主要指 “不一致”, 它在數據I/O中非常有用. 之前的 Windows 示例中主要通過send & recv 函數進行同步 I/O. 調用 send 函數時, 完成數據傳輸後才能從函數返回(確切地說, 只有把數據完全傳輸到輸出緩衝後才能返回); 而調用 recv 函數時, 只有讀到期望大小的數據後才能返回. 因此, 相當於同步方式的 I/O 處理.
在這裏插入圖片描述
各位或許有這種疑問, 但我想反問大家: “哪些部分進行了同步處理? " 同步的關鍵是函數的調用及返回時刻, 以及數據傳輸的開始和完成時刻.
在這裏插入圖片描述
可以通過圖 21-1 解析上述兩句話的含義(上述語句和圖中的"完成傳輸” 都是指數據完全傳輸到輸出緩衝).
在這裏插入圖片描述
相信各位能夠通過上述圖文理解同步的關鍵所在. 那異步 I/O 的含義又是什麼呢? 圖21-2 給出解析, 希望大家與圖21-1 進行對比.
在這裏插入圖片描述
從圖 21-2 中可以看到, 異步 I/O 是指 I/O 函數的返回時刻與數據接收的完成時刻不一致. 如此看來, 我們接觸過異步 I/O. 如果記不清這些內容, 可以回顧第17章 epoll 的異步I/O 部分.

同步 I/O 的缺點及異步方式的解決方案

異步I/O 就是爲了克服同步的缺點而設計的模型. 同步I/O有哪些缺點? 異步方式又是如何解決的呢? 其實, 第17章的最後部分 "條件觸發和邊緣觸發孰優孰劣"中給出過答案. 各位可能因爲忘記這些內容而感到沮喪, 考慮到這一點, 我將以不同的, 更簡單的方式解析. 從圖 21-1 中很容易找到同步I/O的缺點: "進行I/O的過程中函數無法返回, 所以不能執行其他任務! " 而圖22-2 中, 無論數據是否完成交換都返回函數, 這就意味着可以執行其他任務. 所以說 “異步方式能夠比同步方式更有效使用 CPU”.

理解異步通知 I/O 模型

之前分析了同步和異步方式的I/O函數, 確切得說, 分析了同步和異步方式下 I/O 函數返回時間點的差異. 下面我希望擴展討論的對象(同步和異步並不侷限於 I/O ).

本章題目爲 “異步通知I/O模型”, 意爲 “通知I/O” 是以異步方式工作的. 首先了解一下"通知I/O" 的含義:
在這裏插入圖片描述
故名思義, “通知I/O” 是指發生了I/O相關的特定情況. 典型的通知 I/O 模型是 select 方式. 還記得 select 監視的3種情況嗎? 其中具有代表性的就是 “收到數據的情況”. select函數就是從返回調用的函數時通知需要 I/O 處理的, 或可以進行 I/O 處理的情況. 但這種通知是以同步方式進行的, 原因在於, 需要 I/O 或可以進行 I/O 的時間點(簡言之就是 I/O 相關事件發生的時間點) 與 select 函數的返回時間點一致.

相信各位已理解通知 I/O 模型的含義. 與 “select 函數只在需要或可以進行I/O 的情況下返回” 不同, 異步通知I/O 模型中函數的返回與I/O狀態無關. 本章的 WSAEventSelect 函數就是 select 函數的差異版本.
在這裏插入圖片描述
當然需要! 異步通知I/O中, 指定I/O監視對象的函數和實際驗證狀態變化的函數是相互分離的, 因此, 指定監視對象後可以離開執行其他任務, 最後再回來驗證狀態變化. 以上就是通知 I/O 的所有理論, 下面通過具體函數實現該模型.
在這裏插入圖片描述

21.2 理解和實現異步通知 I/O 模型

異步通知 I/O 模型的實現方法有2種: 一種是使用本書介紹的 WSAEventSelect 函數, 另外一種是使用 WSAAsyncSelect 函數. 使用 WSAAsyncSelect 函數時需要指定 Windows 句柄以獲取發生的事件(UI相關內容), 因此本書不會涉及, 但大家要知道這個函數.

WSAEventSelect 函數和通知

如前所述, 告知I/O 狀態變化的操作就是 “通知”, I/O的狀態變化可以分爲不同情況.
在這裏插入圖片描述
這2種情況都意味着發生了需要或可以進行I/O的事件, 我將根據上下文適當混用這些概念.

首先介紹 WSAEventSelect 函數, 該函數用於指定某一套接字爲事件監視對象.
在這裏插入圖片描述
傳入參數s的套接字內只要發生 INetworkEvent 中指定的事件之一, WSAEventSelect 函數就將 hEventObject 句柄所指內核對象改爲 signaled 狀態. 因此, 該函數又稱 “連接事件對象和套接字的函數”.

另外一個重要的事實是, 無論事件發生與否, WSAEventSelect 函數調用後都會直接返回, 所以執行其他任務. 也就是說, 該函數以異步通知方式工作. 下面介紹作爲該函數第三個參數的事件類型信息, 可以通過位或運算同時指定多個信息.
在這裏插入圖片描述
以上就是 WSAEventSelect 函數的調用方法. 各位或許有如下疑問 (很好的問題):
在這裏插入圖片描述
的確, 僅從概念上看, WSAEventSelect 函數的功能偏弱. 但使用該函數時, 沒必要針多個套接字進行調用. 從select 函數返回時, 爲了驗證事件的發生需要再次針對所有句柄(文件描述符)調用函數, 但通過調用 WSAEventSelect 函數傳遞的套接字信息已註冊到操作系統, 所以無需再次調用. 這反而是 WSAEventSelect 函數比select 函數的優勢所在.
在這裏插入圖片描述
從前面關於 WSAEventSelect 函數的說明中可以看出, 需要補充如下內容.
在這裏插入圖片描述
上述過程中只要插入 WSAEventSelect 函數的調用就與服務器端的實現過程完全一致, 下面分別講解.

manual-reset 模式事件對象的其他創建方法

我們之前利用 CreateEvent 函數創建了事件對象. CreateEvent 函數在創建事件對象時, 可以 在auto-reset模式和 manual-reset 模式中任選其一. 我們只需要 manual-reset 模式 non-signaled 狀態的事件對象, 所以利用如下函數傳創建較爲方便.
在這裏插入圖片描述
上述聲明中返回類型 WSAEVENT 的定義如下:
在這裏插入圖片描述
實際上就是我們熟悉的內核對象句柄, 這一點需要注意. 另外, 爲了銷燬通過上述函數創建的事件對象, 系統提供瞭如下函數.
在這裏插入圖片描述

驗證是否發生事件

既然介紹了 WSACreateEvent 函數, 那調用 WSAEventSelect 函數應該不成問題. 接下來就要考慮調用 WSAEventSelect 函數後的處理. 爲了驗證是否發生事件, 需要查看事件對象. 完成該任務的函數如下, 除了多個參數外, 其餘部分與 WaitForMulipleObjects 函數完全相同.
在這裏插入圖片描述
由於發生套接字事件, 事件對象轉爲 signale 狀態後該函數才返回, 所以它非常有利於確認事件發生與否. 但由於最多可傳遞64個事件對象, 如果需要監視更多句柄, 就只能創建線程或擴展保存句柄的數組, 並多次調用上述函數.
在這裏插入圖片描述
對於 WSAWaitForMultipleEvents 函數, 各位可能產生如下疑問:
在這裏插入圖片描述
答案是: 只能通過1次函數無法得到轉爲 signaled 狀態的所有事件對象句柄的信息. 通過該函數可以得到轉爲 signaled 狀態的事件對象中的第一個 (按數組中的保存順序) 索引值. 但可以利用 “事件對象爲manual-reset模式” 的特點, 通過如下方式獲得所有 signaled 狀態的事件對象.
在這裏插入圖片描述
注意觀察上述代碼中的循環. 循環中從第一個事件對象最後一個事件對象逐一次序驗證是否轉爲 signaled 狀態(超時信息爲0, 所以調用函數後立即返回). 之所以能做到這一點, 完全是因爲事件爲 manual-reset 模式, 這也解析了爲何異步通過 I/O 模型中事件對象必須爲 manual-reset 模式.

區分事件類型

既然已經通過 WSAWaitForMultipleEvents 函數得到了轉爲 signaled 狀態的事件對象, 最後就要確定相應對象進入 signaled 狀態的原因. 爲完成該任務, 我們引入如下函數. 調用時, 不僅需要 signaled 狀態的事件對象句柄, 還需要與之連接的(由WSAEventSelect 函數調用引發的) 發生事件的套接字句柄.
在這裏插入圖片描述
上述函數將 manual-reset 模式的事件對象改爲 non-signale 狀態, 所以得到發生的事件類型後, 不必單獨調用ResetEvent 函數. 下面介紹與上述函數有關的 WSANETWORKEVENTS 結構體.
在這裏插入圖片描述
上述結構體的 INetworkEvents 成員將保存發生的事件信息. 與 WSAEventSelect 函數的第三個參數相同, 需要接收數據時, 該成員函數爲 FD_READ; 有連接請求時, 該成員爲 FD_ACCEPT. 因此, 可通過如下方式查看發生的事件類型.
在這裏插入圖片描述
另外, 錯誤信息將保存到聲明爲成員的 iErrorCode 數組(發生錯誤的原因可能很多, 因此用數組聲明). 驗證方法如下.
在這裏插入圖片描述
可通過如下描述理解上述內容.
在這裏插入圖片描述
因此可以用如下 方式檢查錯誤.
在這裏插入圖片描述
以上就是異步通知I/O模型的全部內容, 下面利用這些 知識編寫示例.
在這裏插入圖片描述

利用異步通知 I/O 模型實現回聲服務器端

下面要介紹的回聲服務器端代碼相對偏長, 所以將分爲幾個 部分逐個介紹.
在這裏插入圖片描述

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#define BUF_SIZE 100

void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(const char* msg);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServeSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;

	SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];
	WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
	WSAEVENT newEvent;
	WSANETWORKEVENTS netEvents;

	int numOfClntSock = 0;
	int strLen, i;
	int posInfo, startIdx;
	int clntAdrLen;
	char msg[BUF_SIZE];

	if (argc != 2)
	{
		printf("Usage: %s <port> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}

	hServeSock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServeSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}

	if (listen(hServeSock, 5) == -1)
	{
		ErrorHandling("listen() error");
	}

	newEvent = WSACreateEvent();
	if (WSAEventSelect(hServeSock, newEvent, FD_ACCEPT) == SOCKET_ERROR)
	{
		ErrorHandling("WSAEventSelect() error");
	}

	hSockArr[numOfClntSock] = hServeSock;
	hEventArr[numOfClntSock] = newEvent;
	numOfClntSock++;

	while (1)
	{
		posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
		startIdx = posInfo - WSA_WAIT_EVENT_0;

		for (i = startIdx; i < numOfClntSock; i++)
		{
			int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
			if ((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT))
			{
				continue;
			}
			else
			{
				sigEventIdx = i;
				WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);
				if (netEvents.lNetworkEvents & FD_ACCEPT) /* 請求連接時 */
				{
					if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
					{
						puts("Accept Error");
						break;
					}
					clntAdrLen = sizeof(clntAdr);
					hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR*)&clntAdr, &clntAdrLen);
					newEvent = WSACreateEvent();
					WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);

					hEventArr[numOfClntSock] = newEvent;
					hSockArr[numOfClntSock] = hClntSock;
					numOfClntSock++;
					puts("connected new client...");
				}

				if (netEvents.lNetworkEvents & FD_READ)
				{
					if (netEvents.iErrorCode[FD_READ_BIT] != 0)
					{
						puts("Read Error");
						break;
					}

					strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);
					send(hSockArr[sigEventIdx], msg, strLen, 0);
				}

				if (netEvents.lNetworkEvents & FD_CLOSE) /* 斷開連接 */
				{
					if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0)
					{
						puts("Close Error");
						break;
					}

					WSACloseEvent(hEventArr[sigEventIdx]);
					closesocket(hSockArr[sigEventIdx]);

					numOfClntSock--;
					CompressSockets(hSockArr, sigEventIdx, numOfClntSock);
					CompressEvents(hEventArr, sigEventIdx, numOfClntSock);
				}
			}
		}
	}

	WSACleanup();
	return 0;
}

void CompressSockets(SOCKET hSockArr[], int idx, int total)
{
	int i;
	for (i = idx; i < total; i++)
	{
		hSockArr[i] = hSockArr[i + 1];
	}
}
void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{
	int i;
	for (i = idx; i < total; i++)
	{
		hEventArr[i] = hEventArr[i + 1];
	}
}
void ErrorHandling(const char* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

斷開連接並從數組中刪除套接字以及與之相連的對象時調用上述2個函數(以Compress…開頭), 它們主要用於填充數組空間, 只有同時調用才能維持套接字和事件對象之間的關係.

既然分析了所有代碼, 本應給出運行結果, 但因其與之前的回聲服務器端/客戶端並無差異, 故省略. 另外 , 上述示例可以與任意 回聲客戶端配合運行, 各位可以選擇Windows 平臺下的客戶端作爲配套程序 .

結語:

我最近 買了實體書 , 先看完電子版(先過一遍知識點, 我沒有這麼牛逼能記住, 可以複習的嘛! ), 再買實體版 , 避免它又成爲收藏書沒啥用, 這本書非常適合新手

你可以下面這個網站下載這本書<TCP/IP網絡編程>
https://www.jiumodiary.com/

時間: 2020-06-17

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章