IOCP 浅析与实例
這一年半來一直在做游戲項目邏輯層,學會了不少東西,覺得自己應該看看服務器底層的東西了,主要的東西就是網絡模塊,網絡模塊是沿用以前項目的,在?我們項目中被我們頭改動過幾次,現在還是比較穩定的。因為是Windows平臺,所以用的依然是被大多數人神話了的IOCP,不過的確IOCP?表現的非常不錯。
什么是IOCP?
眾所周知,為了絕對同步,所以很多模式都采用的是同步模式,而不是異步,這樣就會產生很大情況下在等待,CPU在切換時間片,從而導致效率比較低。自從MS在winsocket2中引入了IOCP這個模型之后,他才開始被大家所認知。
IOCP?(I/O?Completion?Port),中文譯作IO完成端口,他是一個異步I/O操作的API,他可以高效的將I/O事件通知給我們的應用程序,那游戲項目來說,就是客戶端或者服務器。
他與Socket基礎API??select()或其他異步方法不同的是,他需要講一個Socket和一個完成端口綁定在一起,然后就可以進行網路通信了。
什么是同步/異步?
所謂同步,就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。按照這個定義,其實絕大多數函數都是同步調用(例如sin,?isdigit等)。
異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。
邏輯上通俗來講是完成一件事再去做另外一件事情就是同步,而一起做兩件或者兩件以上的事情就是異步了。類似于Win32API中的SendMessage()和PostMessage(),你可以將他理解成單線程和多線程的區別。
拿游戲服務器與客戶端通信來說:
如果是同步:
ClientA發送一條Msg1Req消息給Server,這個時候ClientA就會等待Server處理Msg1Req。這段時間內ClientA只有等待,因為Server還沒有給ClientA回復Msg1Ack消息,所以ClientA只能癡癡的等,等到回復之后,才能處理第二條Msg2Req消息,這樣無疑就會大大的降低性能,產生非常差的用戶體驗。
如果是異步:
ClientA發送一條Msg1Req消息給Server,ClientA有發送第二條Msg2Req消息給Server,Server會將他們都存入隊列,一條一條處理,處理完之后回復給ClientA,這樣用戶就可以不必等待,效率就會非常高。
什么是阻塞/非阻塞?
阻塞調用是指調用結果返回之前,當前線程會被掛起。函數只有在得到結果之后才會返回。可能阻塞和同步有點類似,但是同步調用的時候線程還是激活的,而阻塞時線程會被掛起。
非阻塞調用和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。
對象的阻塞模式和阻塞函數調用
對象是否處于阻塞模式和函數是不是阻塞調用有很強的相關性,但是并不是一一對應的。阻塞對象上可以有非阻塞的調用方?式,我們可以通過一定的API去輪詢狀態,在適當的時候調用阻塞函數,就可以避免阻塞。而對于非阻塞對象,調用特殊的函數也可以進入阻塞調用。函數?select就是這樣的一個例子。
對IOCP的評價如何?
I/O完成端口可能是Win32提供的最復雜的內核對象。?Jeffrey?Richter
這是實現高容量網絡服務器的最佳方法。Microsoft?Corporation
完成端口模型提供了最好的伸縮性。這個模型非常適用來處理數百乃至上千個套接字。Anthony?Jones?&?Jim?Ohlund
I/O?completion?ports特別顯得重要,因為它們是唯一適用于高負載服務器[必須同時維護許多連接線路]的一個技術。Completion?ports利用一些線程,幫助平衡由I/O請求所引起的負載。這樣的架構特別適合用在SMP系統中產生的”scalable”服務器。?Jim?Beveridge?&?Robert?Wiener
IOCP中的完成是指什么意思?
網絡通信說白了就是將一堆數據發過來發過去,到底還是數據的操作。不過大家都知道I/O操作是非常慢的,包括打印機、調制解調器、硬盤等,至少相對于CPU來說是非常慢的。坐等I/O是很浪費時間的事情,可能你只需要讀取100KB的數據,假設讀了0.1秒,假設CPU是3.0G?Hz,那么CPU已經運行了0.3G次了,所以CPU這個時候就不滿意了,哥這么NB,為什么要等你?
所以我們用另外一個線程來處理I/O操作,使用重疊IO(Overlapped?I/O)技術,應用程序可以要求OS為其傳輸數據,在完成的時候通知應用程序,然后在進行相應操作,這也就是為什么叫完成的原因。這可以使得應用程序在I/O傳輸期間可以做其他事情,這也可以最大限度的利用線程,而讓最NB的CPU不至于癡癡等待。
下來會將IOCP和網絡有什么關系,以及IOCP的簡單應用。
?
上一篇《IOCP淺析》中翻翻的談了一下IOCP的簡單含義,這篇稍微深入討論下IOCP到底有什么好的,讓大家將他推向神壇,同時簡單的討論下基本函數。
IOCP出現的意義?
寫過網絡程序的朋友應該很清楚網絡程序的原型代碼,startup一個WSADATA,然后建立一個監聽socket對象,綁定一個服務器地址,然后開始監聽,無限循環的accept來自客戶端的消息,建立一個線程來處理消息,accept之后線程就被掛起了,知道收到來自客戶端的消息。
這樣的模型中服務器對每個客戶端都會創建一個線程,優點在于等待請求的線程只做很少的事情,大部分時間該線程都在休息,因為recv函數是阻塞的。
所以這樣的效率并不是很高,NT小組意識到這樣CPU的大部分時間都耗費在線程的上下文切換上,線程并沒有搶到cpu時間來處理自己的工作。
NT小組想到了一個解決辦法,實現開好N個線程,將用戶的消息都投遞到一個消息隊列中去,然后事先開好的N個線程逐一從消息隊列中取出消息并加以處理,就可以避免為每一個客戶端的請求單獨開線程,既減少了線程的資源,也提高了線程的利用率。所以I/O完成端口的內核對象在NT3.5中首次被引入,MS還是比較偉大的。
這里你也看到了,IOCP其實稱作是一種消息處理的機制差不多,而叫完成端口估計也是有歷史原因,亦或者是因為他提供了用戶與操作系統的一種接口吧。
ICOP的基本函數接口
創建完成端口
C++
| 123456 | HANDLE?WINAPI?CreateIoCompletionPort(????__in??????HANDLEFileHandle,????????????????// An open file handle or INVALID_HANDLE_VALUE????__in_opt??HANDLEExistingCompletionPort,????// A handle to an existing I/O completion port or NULL????__in??????ULONG_PTRCompletionKey,????????????// Completion key????__in??????DWORDNumberOfConcurrentThreads????// Number of threads to execute concurrently????); |
第一個參數是指一個已經打開的文件句柄或者空句柄值,一般為客戶端的socket?注意:第一個參數HANDLE在創建時需要在CreateFile()中制定FILE_FLAG_OVERLAPPED標志。
第二個參數是指一個已經存在的IOCP句柄或者NULL
第三個參數是指完成Key,是一個unsigned long的指針,可以為NULL
第四個參數才是我們比較關心的,是指已經創建好的線程數,一般我們會用一個公式來計算,預設的線程數 = CPU核心數 * 2 + 2,有人也說是 + 1,我是沒明白為什么要這樣計算,希望大神指教。
對于第三個參數的意思,MSDN上解釋如下 Use the CompletionKey parameter to help your application track which I/O operations have completed.(用來檢測那些IO操作已經完成)
該函數用于兩個不同的目的
1.創建一個完成端口的句柄對象
HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, m_nIOWorkers);
2.將一個句柄和完成端口關聯在一起
在綁定每一個CLIENT到IOCP時,需要傳遞一個DWORD CompletionKey, 該參數為CLIENT信息的一個指針。
IO的異步調用
C++| 1 2 3 4 5 6 | BOOLWINAPI?PostQueuedCompletionStatus( ??__in??????HANDLECompletionPort, ??__in??????DWORDdwNumberOfBytesTransferred, ??__in??????ULONG_PTRdwCompletionKey, ??__in_opt??LPOVERLAPPEDlpOverlapped ); |
第一個參數為創建的完成端口句柄
第二個參數傳輸了多少字節
第三個參數同樣為完成鍵指針
第四個參數為重疊I/O buffer,其結構如下
C
| 123456789101112 | typedefstruct?_OVERLAPPED{????ULONG_PTRInternal;????ULONG_PTRInternalHigh;????union{????????struct{????????????DWORDOffset;????????????DWORDOffsetHigh;????????};????????PVOID??Pointer;????};????HANDLE????hEvent;}OVERLAPPED,*LPOVERLAPPED; |
線程的同步
C++| 1 2 3 4 5 6 7 | BOOLWINAPI?GetQueuedCompletionStatus( ??__in??HANDLE?CompletionPort, ??__out??LPDWORDlpNumberOfBytes, ??__out??PULONG_PTRlpCompletionKey, ??__out??LPOVERLAPPED*lpOverlapped, ??__in??DWORD?dwMilliseconds ); |
第一個參數為創建的完成端口句柄
第二個參數同樣為傳輸了多少字節
第三個參數同樣為完成鍵指針
第四個參數為重疊I/O buffer
第五個參數的解釋如下
The number of milliseconds that the caller is willing to wait for a completion packet to appear at the completion port. If a completion packet does not appear within the specified time, the function times out, returnsFALSE, and sets *lpOverlapped?toNULL.(等待完成端口上的完成packet出現的毫秒數。如果一個完成在特殊時間內沒有出現,則認為超時,返回false,同時將重疊buffer置為NULL)
大體上函數就有這么幾個,大家有興趣可以去看看MSDN上關于完成端口上的英文介紹,比較準確,中文翻譯上難免出現歧義,同時還可以鍛煉英文閱讀。
好了,I/O完成端口的API就介紹到這里,下一篇會嘗試著寫出一個簡單的完成端口模型來通訊,這篇可能會比較晚出來,因為自己也是摸索階段,大家互勉。
本文是我在學習IOCP的時候,第一次寫一個完整的例子出來,當然了,參考了CSDN上一些朋友的博客,大部分都是按照他們的思路寫的,畢竟我是初學者,參考現成的學起來比較快。當然了,真正用到項目中的IOCP肯定不止這么簡單的,還有內存池,環形緩沖區,socket連接池等高端內容,后面我會參考一些例子,寫出一個完整的給大家看。
C++
| 12345678910111213141516171819202122232425262728293031 | /************************************************************************????????FileName:iocp.h????????Author????:eliteYang????????http://www.cppfans.org************************************************************************/#ifndef __IOCP_H__#define __IOCP_H__#include#include#define DefaultPort 20000#define DataBuffSize 8 * 1024typedefstruct{????OVERLAPPEDoverlapped;????WSABUFdatabuff;????CHARbuffer[?DataBuffSize?];????DWORDbytesSend;????DWORDbytesRecv;}PER_IO_OPERATEION_DATA,*LPPER_IO_OPERATION_DATA;typedefstruct{????SOCKETsocket;}PER_HANDLE_DATA,*LPPER_HANDLE_DATA;#endif |
前面講過IOCP里面一個很重要的東西就是IO重疊了,所以結構體里有一個OVERLAPPED結構。
C++| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | /************************************************************************ ????????FileName:iocp.cpp ????????Author????:eliteYang ????????http://www.cppfans.org ************************************************************************/ #include "iocp.h" #include usingnamespace?std; #pragma comment( lib, "Ws2_32.lib" ) DWORDWINAPI?ServerWorkThread(LPVOID?CompletionPortID); voidmain() { ????SOCKETacceptSocket; ????HANDLEcompletionPort; ????LPPER_HANDLE_DATApHandleData; ????LPPER_IO_OPERATION_DATApIoData; ????DWORDrecvBytes; ????DWORDflags; ????WSADATAwsaData; ????DWORDret; ????if(?ret=?WSAStartup(0x0202,?&wsaData?)?!=?0) ????{ ????????std::cout<<?"WSAStartup failed. Error:"<<?ret<<?std::endl; ????????return; ????} ????completionPort=?CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,?0,?0); ????if(?completionPort==?NULL) ????{ ????????std::cout<<?"CreateIoCompletionPort failed. Error:"<<?GetLastError()<<?std::endl; ????????return; ????} ????SYSTEM_INFOmySysInfo; ????GetSystemInfo(&mySysInfo?); ????// 創建 2 * CPU核數 + 1 個線程 ????DWORDthreadID; ????for(?DWORDi?=0;?i?<(?mySysInfo.dwNumberOfProcessors*?2+?1);?++i?) ????{ ????????HANDLEthreadHandle; ????????threadHandle=?CreateThread(NULL,?0,?ServerWorkThread,completionPort,0,?&threadID?); ????????if(?threadHandle==?NULL) ????????{ ????????????std::cout<<?"CreateThread failed. Error:"<<?GetLastError()<<?std::endl; ????????????return; ????????} ????????CloseHandle(threadHandle?); ????} ????// 啟動一個監聽socket ????SOCKETlistenSocket?=WSASocket(?AF_INET,?SOCK_STREAM,0,?NULL,?0,WSA_FLAG_OVERLAPPED?); ????if(?listenSocket==?INVALID_SOCKET) ????{ ????????std::cout<<?" WSASocket( listenSocket ) failed. Error:"<<?GetLastError()<<?std::endl; ????????return; ????} ????SOCKADDR_INinternetAddr; ????internetAddr.sin_family=?AF_INET; ????internetAddr.sin_addr.s_addr=?htonl(INADDR_ANY?); ????internetAddr.sin_port=?htons(DefaultPort?); ????// 綁定監聽端口 ????if(?bind(listenSocket,(PSOCKADDR)&internetAddr,sizeof(?internetAddr?))?==SOCKET_ERROR?) ????{ ????????std::cout<<?"Bind failed. Error:"<<?GetLastError()<<?std::endl; ????????return; ????} ????if(?listen(listenSocket,5?)==??SOCKET_ERROR) ????{ ????????std::cout<<?"listen failed. Error:"<<?GetLastError()<<?std::endl; ????????return; ????} ????// 開始死循環,處理數據 ????while(?1) ????{ ????????acceptSocket=?WSAAccept(listenSocket,NULL,?NULL,?NULL,0?); ????????if(?acceptSocket==?SOCKET_ERROR) ????????{ ????????????std::cout<<?"WSAAccept failed. Error:"<<?GetLastError()<<?std::endl; ????????????return; ????????} ????????pHandleData=?(LPPER_HANDLE_DATA)GlobalAlloc(GPTR,?sizeof(?PER_HANDLE_DATA)?); ????????if(?pHandleData=?NULL) ????????{ ????????????std::cout<<?"GlobalAlloc( HandleData ) failed. Error:"<<?GetLastError()<<?std::endl; ????????????return; ????????} ????????pHandleData->socket=?acceptSocket; ????????if(?CreateIoCompletionPort((HANDLE)acceptSocket,completionPort,(ULONG_PTR)pHandleData,0?)==?NULL) ????????{ ????????????std::cout<<?"CreateIoCompletionPort failed. Error:"<<?GetLastError()<<?std::endl; ????????????return; ????????} ????????pIoData=?(LPPER_IO_OPERATION_DATA?)GlobalAlloc(GPTR,?sizeof(?PER_IO_OPERATEION_DATA)?); ????????if(?pIoData==?NULL) ????????{ ????????????std::cout<<?"GlobalAlloc( IoData ) failed. Error:"<<?GetLastError()<<?std::endl; ????????????return; ????????} ????????ZeroMemory(&(?pIoData->overlapped),?sizeof(?pIoData->overlapped)?); ????????pIoData->bytesSend=?0; ????????pIoData->bytesRecv=?0; ????????pIoData->databuff.len=?DataBuffSize; ????????pIoData->databuff.buf=?pIoData->buffer; ????????flags=?0; ????????if(?WSARecv(acceptSocket,&(pIoData->databuff),1,?&recvBytes,?&flags,?&(pIoData->overlapped),NULL?)==?SOCKET_ERROR) ????????{ ????????????if(?WSAGetLastError()!=?ERROR_IO_PENDING) ????????????{ ????????????????std::cout<<?"WSARecv() failed. Error:"<<?GetLastError()<<?std::endl; ????????????????return; ????????????} ????????????else ????????????{ ????????????????std::cout<<?"WSARecv() io pending"<<?std::endl; ????????????????return; ????????????} ????????} ????} } DWORD?WINAPI?ServerWorkThread(LPVOID?CompletionPortID) { ????HANDLEcomplationPort?=(HANDLE)CompletionPortID; ????DWORDbytesTransferred; ????LPPER_HANDLE_DATApHandleData?=NULL; ????LPPER_IO_OPERATION_DATApIoData?=NULL; ????DWORDsendBytes?=0; ????DWORDrecvBytes?=0; ????DWORDflags; ????while(?1) ????{ ????????if(?GetQueuedCompletionStatus(complationPort,&bytesTransferred,(PULONG_PTR)&pHandleData,(LPOVERLAPPED*)&pIoData,INFINITE?)==0) ????????{ ????????????std::cout<<?"GetQueuedCompletionStatus failed. Error:"<<?GetLastError()<<?std::endl; ????????????return0; ????????} ????????// 檢查數據是否已經傳輸完了 ????????if(?bytesTransferred==?0) ????????{ ????????????std::cout<<?" Start closing socket..."<<?std::endl; ????????????if(?CloseHandle((HANDLE)pHandleData->socket)?==SOCKET_ERROR?) ????????????{ ????????????????std::cout<<?"Close socket failed. Error:"<<?GetLastError()<<?std::endl; ????????????????return0; ????????????} ????????????GlobalFree(pHandleData?); ????????????GlobalFree(pIoData?); ????????????continue; ????????} ????????// 檢查管道里是否有數據 ????????if(?pIoData->bytesRecv==?0) ????????{ ????????????pIoData->bytesRecv=?bytesTransferred; ????????????pIoData->bytesSend=?0; ????????} ????????else ????????{ ????????????pIoData->bytesSend+=?bytesTransferred; ????????} ????????// 數據沒有發完,繼續發送 ????????if(?pIoData->bytesRecv>?pIoData->bytesSend) ????????{ ????????????ZeroMemory(&(pIoData->overlapped),sizeof(?OVERLAPPED?)); ????????????pIoData->databuff.buf=?pIoData->buffer+?pIoData->bytesSend; ????????????pIoData->databuff.len=?pIoData->bytesRecv-?pIoData->bytesSend; ????????????// 發送數據出去 ????????????if(?WSASend(pHandleData->socket,&(pIoData->databuff),1,?&sendBytes,?0,?&(pIoData->overlapped),NULL?)==?SOCKET_ERROR) ????????????{ ????????????????if(?WSAGetLastError()!=?ERROR_IO_PENDING) ????????????????{ ????????????????????std::cout<<?"WSASend() failed. Error:"<<?GetLastError()<<?std::endl; ????????????????????return0; ????????????????} ????????????????else ????????????????{ ????????????????????std::cout<<?"WSASend() failed. io pending. Error:"<<?GetLastError()<<?std::endl; ????????????????????return0; ????????????????} ????????????} ????????????std::cout<<?"Send "<<?pIoData->buffer<<?std::endl; ????????} ????????else ????????{ ????????????pIoData->bytesRecv=?0; ????????????flags=?0; ????????????ZeroMemory(&(pIoData->overlapped),sizeof(?OVERLAPPED?)); ????????????pIoData->databuff.len=?DataBuffSize; ????????????pIoData->databuff.buf=?pIoData->buffer; ????????????if(?WSARecv(pHandleData->socket,&(pIoData->databuff),1,?&recvBytes,?&flags,?&(pIoData->overlapped),NULL?)==?SOCKET_ERROR) ????????????{ ????????????????if(?WSAGetLastError()!=?ERROR_IO_PENDING) ????????????????{ ????????????????????std::cout<<?"WSARecv() failed. Error:"<<?GetLastError()<<?std::endl; ????????????????????return0; ????????????????} ????????????????else ????????????????{ ????????????????????std::cout<<?"WSARecv() io pending"<<?std::endl; ????????????????????return0; ????????????????} ????????????} ????????} ????} } |
整個過程還是類似于最基礎的socket連接方式,主要部分就是使用IOCP的兩個函數,創建IOCP和檢測當前的狀態。
大家先湊活看吧,后面本博客會有更精彩的IOCP內容呈現給大家,我也是逐步在學習,大家稍安勿躁。
總結
以上是生活随笔為你收集整理的IOCP 浅析与实例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WSAIoctl 函数详解
- 下一篇: 个人IOCP服务器例子解说