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
* 結構體存儲:記錄單個套接字的資料,包括了套接字的變量及套接字的對應的用戶端的位址。
* 結構體作用:當伺服器連接配接上用戶端時,資訊存儲到該結構體中,知道用戶端的位址以便于回訪。
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";
// 建立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;
// 建立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;
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;
// 将SOCKET設定為監聽模式
int listenResult = listen(srvSocket, 10);
if(SOCKET_ERROR == listenResult){
cerr << "Listen failed. Error: " << GetLastError() << endl;
// 開始處理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");
// 建立用來和套接字關聯的單句柄資料資訊結構
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; // read
DWORD 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;
bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);
if(bRet == 0){
cerr << "GetQueuedCompletionStatus Error: " << GetLastError() << endl;
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)); // 清空記憶體
// 發送資訊的線程執行函數
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;
for(int i = 0; i < clientGroup.size(); ++i){
send(clientGroup[i]->socket, talk, 200, 0); // 發送資訊
ReleaseMutex(hMutex);
// IOCP_TCPIP_Socket_Client.cpp
#include <cstdio>
#include <string>
#include <cstring>
#include <winsock2.h>
SOCKET sockClient; // 連接配接成功後的套接字
HANDLE bufferMutex; // 令其能互斥成功正常通信的信号量句柄
const int DefaultPort = 6000;
// 加載socket動态連結庫(dll)
WORD wVersionRequested;
WSADATA wsaData; // 這結構是用于接收Wjndows Socket的結構資訊的
wVersionRequested = MAKEWORD( 2, 2 ); // 請求2.2版本的WinSock庫
int err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) { // 傳回值為零的時候是表示成功申請WSAStartup
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();
}
// 将套接字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'){
else{
cout << "退出系統中...";
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");
DWORD WINAPI SendMessageThread(LPVOID IpParameter)
string talk;
getline(cin, talk);
WaitForSingleObject(bufferMutex, INFINITE); // P(資源未被占用)
if("quit" == talk){
talk.push_back('\0');
send(sockClient, talk.c_str(), 200, 0);
break;
talk.append("\n");
printf("\nI Say:(\"quit\"to exit):");
send(sockClient, talk.c_str(), 200, 0); // 發送資訊
ReleaseSemaphore(bufferMutex, 1, NULL); // V(資源占用完畢)
DWORD WINAPI ReceiveMessageThread(LPVOID IpParameter)
while(1){
char recvBuf[300];
recv(sockClient, recvBuf, 200, 0);
printf("%s Says: %s", "Server", recvBuf); // 接收資訊
五。本次學習資料
幾翻周折,終于寫出一個比較簡單的IOCP模型的伺服器與用戶端啦,并且也大概了解這個模型的思路啦~沒有買書的娃,傷不起啊,隻能從網上搜羅資料,幸好有這些文章在,最後為下列這些文章的作者說聲謝謝~