IOCP模型與網路編程
一。前言:
在老師分配任務(“嘗試利用IOCP模型寫出服務端和用戶端的代碼”)給我時,腦子一片空白,並不知道什麼是IOCP模型,會不會是像軟體設計模式裡面的原廠模式,裝飾模式之類的那些呢?嘿嘿,不過好像是一個挺好玩的東西,挺好奇是什麼東西來的,又是一個新知識啦~於是,開始去尋找一大堆的資料,為這個瞭解做準備,只是呢,有時還是想去找一本書去系統地學習一下,畢竟網路的資料還是有點零散。話說,本人學習這個模型的基礎是,寫過一個簡單的Socket伺服器及用戶端程式,外加一個簡單的Socket單伺服器對多用戶端程式,懂一點點的作業系統原理的知識。於是,本著一個學習與應用的態度開始探究這個IOCP是個什麼東西。
二。提出相關問題:
1. IOCP模型是什嗎?
2. IOCP模型是用來解決什麼問題的?它為什麼存在?
3. 使用IOCP模型需要用到哪些知識?
4. 如何使用IOCP模型與Socket網路編程結合起來?
5. 學會了這個模型以後與我之前寫過的簡單的socket程式主要有哪些不同點?
三。部分問題探究及解決:(絕大多數是個人理解,再加上個人是菜鳥,如果有什麼不對的地方,歡迎指正)
1. 什麼是IOCP?什麼是IOCP模型?IOCP模型有什麼作用?
1) IOCP(I/O Completion Port),常稱I/O完成連接埠。
2) IOCP模型屬於一種通訊模型,適用於(能控制並發執行的)高負載伺服器的一個技術。
3) 通俗一點說,就是用於高效處理很多很多的用戶端進行資料交換的一個模型。
4) 或者可以說,就是能非同步I/O操作的模型。
5) 只是瞭解到這些會讓人很糊塗,因為還是不知道它究意具體是個什麼東東呢?
下面我想給大家看三個圖:
第一個是IOCP的內部工作隊列圖。(整合於《IOCP本質論》文章,在英文的基礎上加上中文對照)
第二個是程式實現IOCP模型的基本步驟。(整合於《深入解釋IOCP》,加個人觀點、理解、翻譯)
第三個是使用了IOCP模型及沒使用IOCP模型的程式流程圖。(個人理解繪製)
2. IOCP的存在理由(IOCP的優點)及技術相關有哪些?
之前說過,很通俗地理解可以理解成是用於高效處理很多很多的用戶端進行資料交換的一個模型,那麼,它具體的優點有些什麼呢?它到底用到了哪些技術了呢?在Windows環境下又如何去使用這些技術來編程呢?它主要使用上哪些API函數呢?呃~看來我真是一個問題多多的人,跟前面提出的相關問題變種延伸了不少的問題,好吧,下面一個個來解決。
1) 使用IOCP模型編程的優點
① 協助維持重複使用的記憶體池。(與重疊I/O技術有關)
② 去除刪除線程建立/終結負擔。
③ 利於管理,分配線程,控制並發,最小化的線程環境切換。
④ 最佳化線程調度,提高CPU和記憶體緩衝的命中率。
2) 使用IOCP模型編程汲及到的知識點(無先後順序)
① 同步與非同步
② 阻塞與非阻塞
③ 重疊I/O技術
④ 多線程
⑤ 棧、隊列這兩種基本的資料結構
3) 需要使用上的API函數
① 與SOCKET相關
1、連結通訊端動態連結程式庫:int WSAStartup(...);
2、建立通訊端庫: SOCKET socket(...);
3、綁字通訊端: int bind(...);
4、通訊端設為監聽狀態: int listen(...);
5、接收通訊端: SOCKET accept(...);
6、向指定通訊端發送資訊:int send(...);
7、從指定通訊端接收資訊:int recv(...);
② 與線程相關
1、建立線程:HANDLE CreateThread(...);
③ 重疊I/O技術相關
1、向通訊端發送資料: int WSASend(...);
2、向通訊端發送資料包: int WSASendFrom(...);
3、從通訊端接收資料: int WSARecv(...);
4、從通訊端接收資料包: int WSARecvFrom(...);
④ IOCP相關
1、建立完成連接埠: HANDLE WINAPI CreateIoCompletionPort(...);
2、關聯完成連接埠: HANDLE WINAPI CreateIoCompletionPort(...);
3、擷取隊列完成狀態: BOOL WINAPI GetQueuedCompletionStatus(...);
4、投遞一個隊列完成狀態:BOOL WINAPI PostQueuedCompletionStatus(...);
四。完整的簡單的IOCP伺服器與用戶端代碼執行個體:
// IOCP_TCPIP_Socket_Server.cpp#include <WinSock2.h>#include <Windows.h>#include <vector>#include <iostream>using namespace std;#pragma comment(lib, "Ws2_32.lib")// Socket編程需用的動態連結程式庫#pragma comment(lib, "Kernel32.lib")// IOCP需要用到的動態連結程式庫/** * 結構體名稱:PER_IO_DATA * 結構體功能:重疊I/O需要用到的結構體,臨時記錄IO資料 **/const int DataBuffSize = 2 * 1024;typedef struct{OVERLAPPED overlapped;WSABUF databuff;char buffer[ DataBuffSize ];int BufferLen;int operationType;}PER_IO_OPERATEION_DATA, *LPPER_IO_OPERATION_DATA, *LPPER_IO_DATA, PER_IO_DATA;/** * 結構體名稱:PER_HANDLE_DATA * 結構體儲存:記錄單個通訊端的資料,包括了通訊端的變數及通訊端的對應的用戶端的地址。 * 結構體作用:當伺服器串連上用戶端時,資訊儲存到該結構體中,知道用戶端的地址以便於回訪。 **/typedef struct{SOCKET socket;SOCKADDR_STORAGE ClientAddr;}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;// 定義全域變數const int DefaultPort = 6000;vector < PER_HANDLE_DATA* > clientGroup;// 記錄用戶端的向量組HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);DWORD WINAPI ServerWorkThread(LPVOID CompletionPortID);DWORD WINAPI ServerSendThread(LPVOID IpParam);// 開始主函數int main(){// 載入socket動態連結程式庫WORD wVersionRequested = MAKEWORD(2, 2); // 請求2.2版本的WinSock庫WSADATA wsaData;// 接收Windows Socket的結構資訊DWORD err = WSAStartup(wVersionRequested, &wsaData);if (0 != err){// 檢查通訊端庫是否申請成功cerr << "Request Windows Socket Library Error!\n";system("pause");return -1;}if(LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){// 檢查是否申請了所需版本的通訊端庫WSACleanup();cerr << "Request Windows Socket Version 2.2 Error!\n";system("pause");return -1;}// 建立IOCP的核心對象/** * 需要用到的函數的原型: * HANDLE WINAPI CreateIoCompletionPort( * __in HANDLE FileHandle,// 已經開啟的檔案控制代碼或者空控制代碼,一般是用戶端的控制代碼 * __in HANDLE ExistingCompletionPort,// 已經存在的IOCP控制代碼 * __in ULONG_PTR CompletionKey,// 完成鍵,包含了指定I/O完成包的指定檔案 * __in DWORD NumberOfConcurrentThreads // 真正並發同時執行最大線程數,一般推介是CPU核心數*2 * ); **/HANDLE completionPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);if (NULL == completionPort){// 建立IO核心對象失敗cerr << "CreateIoCompletionPort failed. Error:" << GetLastError() << endl;system("pause");return -1;}// 建立IOCP線程--線程裡面建立線程池// 確定處理器的核心數量SYSTEM_INFO mySysInfo;GetSystemInfo(&mySysInfo);// 基於處理器的核心數量建立線程for(DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i){// 建立伺服器工作器線程,並將完成連接埠傳遞到該線程HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, completionPort, 0, NULL);if(NULL == ThreadHandle){cerr << "Create Thread Handle failed. Error:" << GetLastError() << endl;system("pause");return -1;}CloseHandle(ThreadHandle);}// 建立流式通訊端SOCKET srvSocket = socket(AF_INET, SOCK_STREAM, 0);// 綁定SOCKET到本機SOCKADDR_IN srvAddr;srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);srvAddr.sin_family = AF_INET;srvAddr.sin_port = htons(DefaultPort);int bindResult = bind(srvSocket, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));if(SOCKET_ERROR == bindResult){cerr << "Bind failed. Error:" << GetLastError() << endl;system("pause");return -1;}// 將SOCKET設定為監聽模式int listenResult = listen(srvSocket, 10);if(SOCKET_ERROR == listenResult){cerr << "Listen failed. Error: " << GetLastError() << endl;system("pause");return -1;}// 開始處理IO資料cout << "本伺服器已準備就緒,正在等待用戶端的接入...\n";// 建立用於發送資料的線程HANDLE sendThread = CreateThread(NULL, 0, ServerSendThread, 0, 0, NULL);while(true){PER_HANDLE_DATA * PerHandleData = NULL;SOCKADDR_IN saRemote;int RemoteLen;SOCKET acceptSocket;// 接收串連,並分配完成端,這兒可以用AcceptEx()RemoteLen = sizeof(saRemote);acceptSocket = accept(srvSocket, (SOCKADDR*)&saRemote, &RemoteLen);if(SOCKET_ERROR == acceptSocket){// 接收用戶端失敗cerr << "Accept Socket Error: " << GetLastError() << endl;system("pause");return -1;}// 建立用來和通訊端關聯的單控制代碼資料資訊結構PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));// 在堆中為這個PerHandleData申請指定大小的記憶體PerHandleData -> socket = acceptSocket;memcpy (&PerHandleData -> ClientAddr, &saRemote, RemoteLen);clientGroup.push_back(PerHandleData);// 將單個用戶端資料指標放到用戶端組中// 將接受通訊端和完成連接埠關聯CreateIoCompletionPort((HANDLE)(PerHandleData -> socket), completionPort, (DWORD)PerHandleData, 0);// 開始在接受通訊端上處理I/O使用重疊I/O機制// 在建立的通訊端上投遞一個或多個非同步// WSARecv或WSASend請求,這些I/O請求完成後,工作者線程會為I/O請求提供服務// 單I/O操作資料(I/O重疊)LPPER_IO_OPERATION_DATA PerIoData = NULL;PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATEION_DATA));ZeroMemory(&(PerIoData -> overlapped), sizeof(OVERLAPPED));PerIoData->databuff.len = 1024;PerIoData->databuff.buf = PerIoData->buffer;PerIoData->operationType = 0;// readDWORD RecvBytes;DWORD Flags = 0;WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);}system("pause");return 0;}// 開始服務背景工作執行緒函數DWORD WINAPI ServerWorkThread(LPVOID IpParam){HANDLE CompletionPort = (HANDLE)IpParam;DWORD BytesTransferred;LPOVERLAPPED IpOverlapped;LPPER_HANDLE_DATA PerHandleData = NULL;LPPER_IO_DATA PerIoData = NULL;DWORD RecvBytes;DWORD Flags = 0;BOOL bRet = false;while(true){bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);if(bRet == 0){cerr << "GetQueuedCompletionStatus Error: " << GetLastError() << endl;return -1;}PerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(IpOverlapped, PER_IO_DATA, overlapped);// 檢查在通訊端上是否有錯誤發生if(0 == BytesTransferred){closesocket(PerHandleData->socket);GlobalFree(PerHandleData);GlobalFree(PerIoData);continue;}// 開始資料處理,接收來自用戶端的資料WaitForSingleObject(hMutex,INFINITE);cout << "A Client says: " << PerIoData->databuff.buf << endl;ReleaseMutex(hMutex);// 為下一個重疊調用建立單I/O操作資料ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED)); // 清空記憶體PerIoData->databuff.len = 1024;PerIoData->databuff.buf = PerIoData->buffer;PerIoData->operationType = 0;// readWSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);}return 0;}// 發送資訊的線程執行函數DWORD WINAPI ServerSendThread(LPVOID IpParam){while(1){char talk[200];gets(talk);int len;for (len = 0; talk[len] != '\0'; ++len){// 找出這個字元組的長度}talk[len] = '\n';talk[++len] = '\0';printf("I Say:");cout << talk;WaitForSingleObject(hMutex,INFINITE);for(int i = 0; i < clientGroup.size(); ++i){send(clientGroup[i]->socket, talk, 200, 0);// 發送資訊}ReleaseMutex(hMutex); }return 0;}
// IOCP_TCPIP_Socket_Client.cpp#include <iostream>#include <cstdio>#include <string>#include <cstring>#include <winsock2.h>#include <Windows.h>using namespace std;#pragma comment(lib, "Ws2_32.lib")// Socket編程需用的動態連結程式庫SOCKET sockClient;// 串連成功後的通訊端HANDLE bufferMutex;// 令其能互斥成功正常通訊的訊號量控制代碼const int DefaultPort = 6000;int main(){// 載入socket動態連結程式庫(dll)WORD wVersionRequested;WSADATA wsaData;// 這結構是用於接收Wjndows Socket的結構資訊的wVersionRequested = MAKEWORD( 2, 2 );// 請求2.2版本的WinSock庫int err = WSAStartup( wVersionRequested, &wsaData );if ( err != 0 ) {// 傳回值為零的時候是表示成功申請WSAStartupreturn -1;}if ( LOBYTE( wsaData.wVersion ) != 2 ||HIBYTE( wsaData.wVersion ) != 2 ) { // 檢查版本號碼是否正確WSACleanup( );return -1; }// 建立socket操作,建立流式通訊端,返回通訊端號sockClient sockClient = socket(AF_INET, SOCK_STREAM, 0); if(sockClient == INVALID_SOCKET) { printf("Error at socket():%ld\n", WSAGetLastError()); WSACleanup(); return -1; } // 將通訊端sockClient與遠程主機相連// int connect( SOCKET s, const struct sockaddr* name, int namelen);// 第一個參數:需要進行串連操作的通訊端// 第二個參數:設定所需要串連的地址資訊// 第三個參數:地址的長度SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// 本地迴路地址是127.0.0.1; addrSrv.sin_family = AF_INET;addrSrv.sin_port = htons(DefaultPort);while(SOCKET_ERROR == connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR))){// 如果還沒串連上伺服器則要求重連cout << "伺服器串連失敗,是否重新串連?(Y/N):";char choice;while(cin >> choice && (!((choice != 'Y' && choice == 'N') || (choice == 'Y' && choice != 'N')))){cout << "輸入錯誤,請重新輸入:";cin.sync();cin.clear();}if (choice == 'Y'){continue;}else{cout << "退出系統中...";system("pause");return 0;}}cin.sync();cout << "本用戶端已準備就緒,使用者可直接輸入文字向伺服器反饋資訊。\n";send(sockClient, "\nAttention: A Client has enter...\n", 200, 0);bufferMutex = CreateSemaphore(NULL, 1, 1, NULL); DWORD WINAPI SendMessageThread(LPVOID IpParameter);DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter);HANDLE sendThread = CreateThread(NULL, 0, SendMessageThread, NULL, 0, NULL); HANDLE receiveThread = CreateThread(NULL, 0, ReceiveMessageThread, NULL, 0, NULL); WaitForSingleObject(sendThread, INFINITE); // 等待線程結束closesocket(sockClient);CloseHandle(sendThread);CloseHandle(receiveThread);CloseHandle(bufferMutex);WSACleanup();// 終止對通訊端庫的使用printf("End linking...\n");printf("\n");system("pause");return 0;}DWORD WINAPI SendMessageThread(LPVOID IpParameter){while(1){string talk;getline(cin, talk);WaitForSingleObject(bufferMutex, INFINITE);// P(資源未被佔用) if("quit" == talk){talk.push_back('\0');send(sockClient, talk.c_str(), 200, 0);break;}else{talk.append("\n");}printf("\nI Say:(\"quit\"to exit):");cout << talk;send(sockClient, talk.c_str(), 200, 0);// 發送資訊ReleaseSemaphore(bufferMutex, 1, NULL);// V(資源佔用完畢) }return 0;}DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter){while(1){char recvBuf[300];recv(sockClient, recvBuf, 200, 0);WaitForSingleObject(bufferMutex, INFINITE);// P(資源未被佔用) printf("%s Says: %s", "Server", recvBuf);// 接收資訊ReleaseSemaphore(bufferMutex, 1, NULL);// V(資源佔用完畢) }return 0;}
五。本次學習資料
幾翻周折,終於寫出一個比較簡單的IOCP模型的伺服器與用戶端啦,並且也大概瞭解這個模型的思路啦~沒有買書的娃,傷不起啊,只能從網上搜羅資料,幸好有這些文章在,最後為下列這些文章的作者說聲謝謝~