TCP/IP網絡編程_基於Windows的編程_第22章重疊I/O模型

在這裏插入圖片描述

22.1 理解重疊 I/O 模型

第21章異步處理的並非I/O, 而是"通知". 本章講解的纔是以異步方式處理 I/O 的方法. 只有理解了二者的區別和各自的優勢, 才能更輕鬆地學習第23章的 IOCP.

重疊 I/O

其實各位對於重疊 I/O 並不陌生. 大家已經掌握了異步 I/O. 我通過圖21-2說明過異步I/O模型, 實際上, 這種異步 I/O 就相當於 重疊I/O. 下面我將給出重疊I/O, 各位可自行判斷二者是否相似. 圖22-1 給出重疊 I/O 的原理.
在這裏插入圖片描述
如圖22-1 所示, 同一線程內部向多個目標傳輸(或從多個目標接收)數據引起的I/O重疊現象 稱爲 “重疊I/O”. 爲了完成這項任務, 調用的I/O函數應立即返回, 只有這樣才能發送後續數據. 從結果上看, 利用上述模型收發數據時, 最重要的前提條件就是異步I/O. 而且, 爲了完成異步I/O, 調用的I/O函數應以非阻塞模式工作.

接下來的判斷交給各位. 異步I/O和重疊I/O之間存在差異衆說, 關鍵是要理解二者的關係. 異步方式進行I/O處理時, 即使不採用本章介紹的方式, 也可以通過其他方法構造如圖 22-1 所示的 I/O 處理方式. 因此, 我認爲不用明確區分.

本章討論的重疊 I/O 的重點不在於 I/O

前面對異步 I/O 和重疊I/O進行了比較, 這些內容看似是本章的全部理論說明, 但其實還未進入重疊I/O的正題. 因爲 Windows 中重疊I/O的重點並非 I/O 本身, 而是如何確認I/O完成時的狀態. 不管是輸入還是輸出, 只要是非阻塞模式的, 就要另外確認執行結果. 關於這種確認方法我們還一無所知. 確認執行結果前需要經過特殊的處理過程, 這是本章要講述的內容. Windows 中的重疊 I/O 不僅包含圖 22-1 所示的I/O(這是基礎). 還包含確認 IO 完成狀態的方法.
在這裏插入圖片描述

創建重疊 I/O 套接字

首先要創建適用於重疊 I/O 的套接字, 可以通過如下函數完成.
在這裏插入圖片描述
各位對前3個參數比較熟悉, 第四個和第5個參數與目前的工作無關, 可以簡單設置爲 NULL 和 0. 可以向最後一個參數傳遞 WSA_FLAG_OVERLAPPED, 賦予創建出的套接字重疊 I/O 特性. 總之, 可以通過如下函數調用創建出可以進行重疊 I/O 的非阻塞模式的套接字.
在這裏插入圖片描述

執行重疊 I/O 的 WSASend 函數

創建出具有重疊 I/O 屬性的套接字後, 接下來2個套接字(服務器/客戶端之間的) 連接過程與一般的套接字連接過程相同, 但 I/O 數據時使用的函數不同. 先介紹重疊 I/O 使用的數據輸出函數.
在這裏插入圖片描述
接下來介紹上述函數的第二個結構體參數類型, 該結構體中存有待傳輸數據的地址和大小等信息.
在這裏插入圖片描述
下面給出上函數的調用示例. 利用上函數傳輸數據時可以按如下方式編寫代碼.
在這裏插入圖片描述
調用 WSASend 函數時將第三個參數設置爲1, 因爲第二個參數中待傳輸數據的緩衝個數爲1. 另外, 多餘參數均設置爲 NULL 或 0, 其中需要注意第六個和第7個參數(稍後將具體解析, 現階段只需留意即可). 第六個參數中的 WSAOVERLAPPED 結構體定義如下.
在這裏插入圖片描述
Internal , InternalHigh 成員是進行重疊 I/O 時操作系統內部使用的成員, 而Offset , OffsetHigh 同樣屬於具有特殊用途的成員. 所以各位實際只需關注 hEvent 成員, 稍後將介紹成員的使用方法.
在這裏插入圖片描述
如果向 IpOverlapped 傳遞NULL, WSASend 函數的第一個參數中參數中的句柄所指的套接字將阻塞模式工作. 還需要了解一下這個事實, 否則也會影響開發.
在這裏插入圖片描述
這是因爲, 進行重疊I/O的過程中, 操作系統將使用 WSAOVERLAPPED 結構體變量.

關於 WSASend 再次補充一點

前面談到, 通過WSASend 函數的 IpNumberberOfBytesSent 參數可以獲得實際傳輸的數據大小. 各位關於這一點不感到困惑嗎?
在這裏插入圖片描述
實際上, WSASend 函數調用過程中, 函數返回時間點和數據傳輸完成點並非總不一致. 如果輸出緩衝是空的, 且傳輸的數據並不大, 那麼函數調用後可以立即完成數據傳輸. 此時, WSASend 函數將返回0, 而 lpNumberOfBytesSent 中將保存實際傳輸的數據大小信息. 反之, WSASend 函數返回仍需要傳輸數據時, 將返回 SOCKET_ERROR, 並將 WSA_IO_PENDING註冊爲錯誤代碼, 該代碼可以通過 WSAGetLastError 函數(稍後再介紹) 得到. 這時應該通過如下函數獲取實際傳輸的數據大小.
在這裏插入圖片描述
通過此函數不僅可以獲取數據傳輸結果, 還可以驗證接收數據的狀態. 如果給出示例前進行過多理論說明會使人感到乏味, 所以稍後將通過示例講解此函數的使用方法.

進行重疊I/O 的 WSAPRecv 函數

有了 WSASend 函數的基礎, WSARecv 函數將不難理解. 因爲他們大同小異, 只是在功能上有接收和傳輸之分.
在這裏插入圖片描述
關於上述函數的使用方法將同樣結合示例進行說明.
以上就是重疊 I/O 中的數據 I/O 方法, 下一節將介紹 I/O 完成及如何確認結果.
在這裏插入圖片描述

22.2 重疊 I/O 的 I/O 完成確認

重疊 I/O 中有2種方法確認 I/O 的完成並獲取結果.
在這裏插入圖片描述
只有理解了這2種方法, 才能算是掌握了重疊 I/O (其實比22.1節更重要). 首先介紹利用第六個參數的方法.

使用事件對象

之前已經介紹了 WSASend, WSARecv 函數的第六個參數 – WSAOVERLAPPED 結構體, 因此直接給出示例. 希望各位通過該示例驗證如下2點.
在這裏插入圖片描述
需要說明的是, 該示例的目的在於整理之前的系列知識點. 因此, 推薦各位在此基礎上自行編寫可以體現重疊I/O 優點的示例.
在這裏插入圖片描述

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

void ErrorHandling(const char* msg);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN sendAdr;

	WSABUF dataBuf;
	char msg[] = "Network is Computer!";
	unsigned long sendBytes = 0;

	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;

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

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

	hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&sendAdr, 0, sizeof(sendAdr));
	sendAdr.sin_family = AF_INET;
	sendAdr.sin_addr.s_addr = inet_addr(argv[1]);
	sendAdr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error");
	}

	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	dataBuf.len = strlen(msg) + 1;
	dataBuf.buf = msg;

	if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			puts("Background data send");
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
		}
		else
		{
			ErrorHandling("WSASend() error");
		}
	}

	printf("Send data size: %d \n", sendBytes);
	WSACloseEvent(evObj);
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

上述示例的第44行調用的 WSAGetLasError 函數定義如下. 調用套接字相關函數後, 可以通過該函數獲取錯誤信息.
在這裏插入圖片描述
上述示例中該函數的返回值爲 WSA_IO_PENDING, 由此可以判斷 WSASend 函數的調用結果並非發生了錯誤, 而是尚未完成(Pendling) 的狀態. 下面介紹與上述示例配套使用 Receiver, 該示例的結構與之前的 Sender 類似.
在這裏插入圖片描述

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

#define BUF_SIZE 1024

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hLisnSock, hRecvSock;
	SOCKADDR_IN lisnAdr, recvAdr;
	int recvAdrSz;

	WSABUF dataBuf;
	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;

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

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

	hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&lisnAdr, 0, sizeof(lisnAdr));
	lisnAdr.sin_family = AF_INET;
	lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	lisnAdr.sin_port = htons(atoi(argv[1]));

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

	if (listen(hLisnSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error");
	}

	recvAdrSz = sizeof(recvAdr);
	hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);

	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = buf;

	if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			puts("Background data receive");
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, NULL);
		}
		else
		{
			ErrorHandling("WSARecv() error");
		}
	}

	printf("Received message: %s \n", buf);
	WSACloseEvent(evObj);
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('/n', stderr);
	exit(1);
}

運行結果:
在這裏插入圖片描述

使用 Completion Routine 函數

前面的示例通過事件對象驗證了 I/O 完成與否, 下面介紹如何通過 WSASend, WSARecv 函數的最後一個參數中指定的 Completion Routine (一下簡介CR) 函數驗證I/O 完成的情況. “註冊CR” 具體有如下 含義:
在這裏插入圖片描述
I/O 完成時調用註冊過的函數進行事後處理, 這就是 Completion Routine 的運作 方式. 如果執行重要任務時突然調用 Completin Routinue, 則有可能破壞程序的正常執行流. 因此, 操作系統通常會預先定義規則:
在這裏插入圖片描述
“alertable wait 狀態” 是 等待 接收操作系統信息的線程狀態. 調用下列 函數進入 altertable wait 狀態.
在這裏插入圖片描述
第一 , 第二, 第四個函數提供的功能與 WaitForSingleObject, WaitForMultipleObjeects, Sleep 函數相同. 上述 函數只增加了1個參數, 如果該函數爲TRUE, 則相應線程將 進入 alertalbe wait 狀態. 另外, 第21章介紹過的 WSA 爲前綴的函數, 該函數的最後一個參數設置爲TRUE 時, 線程同步樣 進入 alertable wait 狀態. 因此, 啓動I/O 任務後, 執行完緊急任務時可以調用上述任一函數驗證I/O 完成與 否. 此時操作系統知道線程進入 alertable wait 狀態. 如果有 已完成 的I/O, 則調用相應 Completion Routine 函數. 調用後, 上述函數將全部返回 WAIT_IO_COMPLETION, 並開始執行接下 來的程序.

以上就是 Completion Roution 函數相關的全部理論說明. 下面將之前的OverlappedRoutine 函數相關的全部理論說明. 下面將之前的OverlappedRecv_win.c 改爲 Completion Routine 方式.
在這裏插入圖片描述

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

#define BUF_SIZE 1024

void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(const char* message);

WSABUF dataBuf;

char buf[BUF_SIZE];
unsigned long recvBytes = 0;

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hLisnSock, hRecvSock;
	SOCKADDR_IN lisnAdr, recvAdr;

	WSAOVERLAPPED overlapped;
	WSAEVENT evObj;

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

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

	hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&lisnAdr, 0, sizeof(lisnAdr));
	lisnAdr.sin_family = AF_INET;
	lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	lisnAdr.sin_port = htons(atoi(argv[1]));

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

	if (listen(hLisnSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error");
	}

	recvAdrSz = sizeof(recvAdr);
	hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
	if (hRecvSock == INVALID_SOCKET)
	{
		ErrorHandling("accept() error");
	}

	memset(&overlapped, 0, sizeof(overlapped));
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = buf;
	evObj = WSACreateEvent();

	if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) 
		== SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			puts("Background data receive");
		}
	}

	idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);
	if (idx == WAIT_IO_COMPLETION)
	{
		puts("Overlapped I/O Completed");
	}
	else
	{
		ErrorHandling("WSARecv() error");
	}

	WSACloseEvent(evObj);
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void CALLBACK CompRoutine(
	DWORD dwError, DWORD szBecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD falgs)
{
	if (dwError != 0)
	{
		ErrorHandling("ComRoutine error");
	}
	else
	{
		recvBytes = recvBytes;
		printf("Received message: %s \n", buf);
	}
}
void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

運行結果:
在這裏插入圖片描述
下面給出傳入 WSARecv 函數的最後一個參數的 Completion Routinue 函數原型.
在這裏插入圖片描述
其中第一個參數中寫入錯誤信息(正常結束時寫入0), 第二個參數中寫入實際收發的字節數. 第三個參數中寫入 WSASend , WSARecv 函數的參數 lpOverlapped, dwFlags 中寫入 調用I/O函數時傳入的特性信息或0. 另外, 返回值類型 void 後插入的CALLBACK 關鍵字與 main 函數中聲明的關鍵字 WINAPI 相同. 都是用於函數的調用規範, 所以定義Completion Routinue 函數時必須添加.

本章介紹了不少內容, 這些都是爲了理解第端3章 IOCP 而講解的. 可以 這說, 本章是學習第23章的必要條件, 請各位務必=掌握本章的內容.
在這裏插入圖片描述

結語:

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

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

時間: 2020-06-18

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