23.1 通過重疊 I/O 理解 IOCP
本章的 IOCP (Input Output Completion Port, 輸入輸出完成端口) 服務器端模型是很多 Windows 程序員關注的焦點. 各位若急於求成而跳過了第21章內容, 建議大家最好回顧一下. 因爲第21章和22章介紹的背景知識, 而且, 關於 IOCP 的內容實際上從第22章開始的.
熱門話題: epoll 和 IOCP 的性能比較
爲了突破 select 等傳統I/O模型的限制, 每種操作系統(內核級別) 都會提供特有的 I/O 模型以提高性能. 其中最具有代表性的有 Linux 的 epoll, BSD 的 kqueue 及本章的 Windows 的 IOCP. 他們都在操作系統級別支持並完成功能, 但此處有一個持續的爭議熱點:
對此的爭議仍然可以在 www.yahoo.com 上看到, 有時甚至會走入極端. 因爲服務器端的響應時間和併發服務數是權衡服務器端好壞的重要因素, 所以存在這種爭議也可以理解. 但對於我這種普通人來說, 這2種模型已經非常優秀了, 因此幾乎不會如下這種情況. 至少我和我周圍的開發人員之間未曾有過這類討論.
另外, 在硬件性能和分配帶寬充足的情況下, 如果響應和併發數量出了問題, 我會首先懷疑如下兩點, 修改後通常會解決大部分問題.
如果有人問爲何 IOCP 相對更優 (通常是網上說的言論), 我會回答: “不太清楚” 雖然 IOCP 擁有其他 I/O 模型不具備的優點, 但這並非左右服務端性能的絕對因素, 而且不可能在任何情況下都體會這種優點. 他們之間的差異主要在於操作系統內部的工作機制. 當然, 最終還是要靠各位自行判斷. 我只是想說, 周圍也有不少開發人員與我觀點一致.
實現非阻塞模式的套接字
第22章中介紹了執行重疊 I/O 的 Sender 和 Receiver, 但還未利用該模型實現過服務器端. 因此, 我們先利用重疊 I/O 模型實現回聲服務器端. 首先介紹創建非阻塞模式套接字的方法. 我們曾在第17章創建過非阻塞模式的套接字, 與之類似, 在 Windows 中通過如下函數調用將套機字屬性改爲非阻塞模式.
上述代碼中調用的 ioctlsocket 函數負責控制套接字 I/O 方式, 其調用具有如下含義:
也就是說, FIONBIO 是用於更改套機字 I/O 模式的選項, 該函數的第三個參數中傳入的變量中若存有0, 則說明套接字是阻塞模式的; 如果存有非0值, 則說明已將套接字模式改爲非阻塞模式. 改爲非阻塞模式後, 除了以非阻塞模式進行 I/O 外, 還具有如下特點.
因此, 針對非阻塞套接字調用 accept 函數, 並返回 INVALID_SOCKET 時, 應該通過 WSAGetLastError 函數確定返回 INVALID_SOCKET 的理由, 在進行適當的處理.
以純重疊 I/O 方式實現回聲服務器端
要想實現基於重疊 I/O 的服務器端, 必須具備非阻塞套接字, 所以先介紹及創建方法. 實際上, 因爲有 IOCP 模型, 所以很少有人只用重疊 I/O 實現服務器端. 但我認爲: “爲了確認理解 IOCP, 應當嘗試用純重疊 I/O 方式實現服務端.”
即使堅持不用 IOCP, 也應具備僅用重疊 I/O 方式實現類似 IOCP 方式實現類似 IOCP 的服務器端的能力. 這樣就可以在其他操作系統平臺實現類似 IOCP 方式的服務端, 而且不會因 IOCP 的限制而忽略服務端功能的實現.
下面用純重疊 I/O 模型實現回聲服務器端, 希望各位親自動手試試. 接下來介紹示例, 由於代碼量較大, 我們分3部分學習.
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#define BUF_SIZE 1024
void CALLBACK ReadCompRoution(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteComRoution(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);
typedef struct
{
SOCKET hClntSock;
char buf[BUF_SIZE];
WSABUF wsaBuf;
}PER_IO_DATA, *LPPER_IO_DATA;
代碼說明: 第11行請注意觀察此處的結構體. 該結構體包含套接字句柄, 緩衝相關信息.
該結構體中的信息足夠進行數據交換, 下列代碼將介紹該結構體的填充及使用方法.
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#define BUF_SIZE 1024
void CALLBACK ReadCompRoution(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteComRoution(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(const char* message);
typedef struct
{
SOCKET hClntSock;
char buf[BUF_SIZE];
WSABUF wsaBuf;
}PER_IO_DATA, * LPPER_IO_DATA;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
LPWSAOVERLAPPED lpOvLp;
DWORD recvBytes;
LPPER_IO_DATA hbInfo;
unsigned long mode = 1;
int recvAdrSz = 0;
unsigned long flagInfo = 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);
ioctlsocket(hLisnSock, FIONBIO, &mode); /* for non-blocking mode socket */
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);
while (1)
{
SleepEx(100, TRUE); /* for alertable wait state */
hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
if (hRecvSock == INVALID_SOCKET)
{
if (WSAGetLastError() == WSAEWOULDBLOCK)
{
continue;
}
else
{
ErrorHandling("accept() error");
}
}
puts("Client connected ...");
lpOvLp = (LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));
hbInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
hbInfo->hClntSock = (DWORD)hRecvSock;
(hbInfo->wsaBuf).buf = hbInfo->buf;
(hbInfo->wsaBuf).len = BUF_SIZE;
lpOvLp->hEvent = (HANDLE)hbInfo;
WSARecv(hRecvSock, &(hbInfo->wsaBuf),
1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoution);
}
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
最後介紹2個Completion Routine 函數. 實際的回聲服務是通過這2個函數完成的, 希望各位仔細觀察提供服務的過程.
void CALLBACK ReadCompRoutine(
DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
SOCKET hSock = hbInfo->hClntSock;
LPWSABUF bufInfo = &(hbInfo->wsaBuf);
DWORD sentBytes;
if (szRecvBytes == 0)
{
closesocket(hSock);
free(lpOverlapped->hEvent);
free(lpOverlapped);
puts("Client disconnected ...");
}
else
{
bufInfo->len = szRecvBytes;
WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteComRoution);
}
}
void CALLBACK WriteComRoution(
DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
SOCKET hSock = hbInfo->hClntSock;
LPWSABUF bufInfo = &(hbInfo->wsaBuf);
DWORD recvBytes;
unsigned long flagInfo = 0;
WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
上述示例的工作原理整理如下.
通過交替調用 ReadCompRoutine 函數和 WriteCompRoutine 函數, 反覆執行數據的接收和發送操作. 另外, 每次增加1個客戶端都會定義 PER_IO_DATA結構體, 以便將新創建的套接字句柄和緩衝信息傳遞給 ReadCompRoutine 函數和 WriteCompRoutine 函數. 同時將該結構體地址值寫入 WSAOVERLAPPED 結構體成員 hEvent, 並傳遞給 Completion
Routine 函數. 這非常重要, 可概括如下:
接下來需要驗證運行結果, 先要編寫回聲客戶端, 因爲使用第4章的回聲客戶端會無法得到預想的結果.
重新實現客戶端
其實第4章實現並使用至今的回聲客戶端存在一些問題, 關於這些問題及解決方案已在第5章進行了充分講解. 雖然在目前爲止的各種模型的服務器端中使用稍有缺陷的回聲客戶端也不會引起太大的問題, 但本章的回聲服務器端則不同. 因此, 需要按照第5章的提示解決客戶端存在的問題, 並結合改進後的客戶端運行本章服務端. 之前已介紹過解決方法, 故只給出代碼.
#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 hSocket;
SOCKADDR_IN servAdr;
char message[BUF_SIZE];
int strLen, readLen;
if (argc != 3)
{
printf("Usage : %s <IP> <port> \n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("connect() error");
}
else
{
puts("Connected ...");
}
while (1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
{
break;
}
strLen = strlen(message);
send(hSocket, message, strLen, 0);
readLen = 0;
while (1)
{
readLen += recv(hSocket, &message[readLen], BUF_SIZE - 1, 0);
if (readLen >= strLen)
{
break;
}
}
message[strLen] = 0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
上述代碼第44行的循環語句考慮到 TCP 的傳輸特性而重複調用了 recv 函數, 直到接收所有數據, 將上述客戶端結合之前的回聲服務器端運行可以得到正確的有運行結果, 具體結果與一般的回聲客戶端服務器端 沒有區別, 故省略.
運行結果:
從重疊 I/O 模型到 IOCP 模型
下面分析重疊 I/O 模型回聲服務器端的缺點.
如果正確理解了之前的示例, 應該不難發現這一點. 既然爲了處理連接請求而只調用accept 函數, 也不能爲了 Completion Routine 而只調用 SleepEx 函數, 因此輪流調用了 非阻塞模式的 accept 函數 和 SleepEx 函數(設置較短的超時時間). 這恰恰是影響性能的代碼結構.
我也不知道如何你補這一缺點, 這屬於重疊 I/O 結構固有的缺陷, 但可以考慮如下方法:
其實這就是 IOCP 中採用的服務器端模型. 換言之, IOCP 將創建專用的 I/O 線程, 該線程負責與所有客戶端進行 I/O .
23.2 分段實現 IOCP 程序
本節我們編寫最後一種服務器模型 IOCP, 比閱讀代碼更重要的是理解 IOCP 本身.
創建 “完成端口”
IOCP 中已完成的 I/O 信息將註冊到完成端口對象(Completion Port, 簡稱CP 對象), 但這個過程並非單純的註冊, 首先需要經過如下請求過程:
該過程稱爲 “套機字和CP對象之間的連接請求”. 因此, 爲了實現基於 IOCP 模型的服務器端, 需要如下2項工作.
此時的套接字必須賦予重疊屬性. 上述2項工作可以通過1個函數完成, 但爲了創建 CP 對象, 先介紹如下函數.
以創建CP對象爲目的調用上述函數時, 只有最後一個參數才真正具有含義. 可以用如下代碼段將分配給 CP 對象的用於處理 I/O 的線程指定爲2.
連接完成端口對象和套接字
既然有了 CP 對象, 接下來就要將該對象連接到套接字, 只有這樣才能使已完成的套接字 I/O 信息註冊到 CP 對象. 下面以創建連接爲目的再次介紹 CreateIoCompetionPort 函數.
上述函數的第二種功能就是將 FileHandle 句柄指向的套機字和 ExistingCompletionPort 指向的 CP 對象相連. 該函數的調用方式如下.
調用 CreateIoCompletionPort 函數後, 只要針對 hSock 的I/O完成, 相關信息就將註冊到 hCpObject 指向的 CP對象.
確認完成端口已完成的 I/O 和 線程的 I/O 處理
我們已經掌握了 CP 對象的創建及其與套接字建立連接的方法, 接下來就要學習如何確認 CP 中註冊的已完成的 I/O. 完成該功能的函數如下.
雖然只介紹了2個 IOCP 相關函數, 但依然有些複雜, 特別是上述函數的第三個和第四個參數更是如此. 其實這2個參數主要是爲了獲取需要的信息而設置的, 下面介紹這2中信息的含義.
各位需要通過示例理解這2個參數的使用方法. 接下來討論其調用主體, 究竟由誰(何時)調用上述函數比較合理呢? 如各位所料, 應該由處理 IOCP 中已完成 I/O 的線程調用.
如前所述, IOCP 中創建全職 I/O 線程, 由該線程針對所有客戶端進行 I/O. 而且 CreateIoCompletionPort 函數中也有參數用於指定分配給 CP 對象的最大線程數, 所以各位或許會有如下疑問:
當然不是! 應該由程序員自行創建調用WSASend, WSARecv 等 I/O 函數的線程, 只是該線程爲了確認 I/O 的完成會調用 GetQueuedCompletionStatus 函數. 雖然任何線程都能調用GetQueued-CompletionStatus 函數, 但實際得到 I/O 完成信息的線程數不會超過調用 CreateloCompletionStatus 函數時指定的最大線程數. 相信大家也理解了分配給 CP 對象線程具有的含義.
以上就是 IOCP 服務端實現需要的全部函數及其理論說明, 下面通過源代碼理解程序的整體結構.
實現基於 IOCP 的回聲服務器端
雖然介紹了 IOCP 相關的理論知識, 但離開示例很難真正掌握 IOCP 的使用方法. 因此, 我將介紹便於理解和運用的(極爲普通的) 基於 IOCP 的回聲服務器端. 首先給出 IOCP 回聲服務器端的 main 函數之前的部分.
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <Windows.h>
#include <WinSock2.h>
#define BUF_SIZE 100
#define READ 3
#define WRITE 5
typedef struct /* socket info */
{
SOCKET hClntSock;
SOCKADDR_IN clntAdr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
typedef struct /* buffer info */
{
OVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUF_SIZE];
int rwMode;
}PER_IO_DATA, *LPPER_IO_DATA;
DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
HANDLE hComPort;
SYSTEM_INFO sysInfo;
LPPER_IO_DATA ioInfo;
LPPER_HANDLE_DATA handleInfo;
SOCKET hServSock;
SOCKADDR_IN servAdr;
int recvBytes, i, flags = 0;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
GetSystemInfo(&sysInfo);
for (i = 0; i < sysInfo.dwNumberOfProcessors; i++)
{
_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);
}
}
這裏有Bug , 等我功力一定, 再搞定, 不能卡在這裏, 不因爲一棵樹, 放棄整片深林
結語:
我最近 買了實體書 , 先看完電子版(先過一遍知識點, 我沒有這麼牛逼能記住, 可以複習的嘛! ), 再買實體版 , 避免它又成爲收藏書沒啥用, 這本書非常適合新手
你可以下面這個網站下載這本書<TCP/IP網絡編程>
https://www.jiumodiary.com/
時間: 2020-06-22