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