現在基於iocp (input/output completion port)的文章其實已經很多了,但是那些文章都不太容易理解,主要是因為iocp本身的一些不易理解的東西,並且沒有相關的能夠說明該技術和**示例的標準文件。因此我決定做乙個簡單的高併發iocp的例子(oiocpnet
),並且提供詳細文件說明處理iocp的操作,以及相關的關鍵問題。實現目標的關鍵思想:
iocp技術
首先第乙個問題是我們為什麼要使用iocp技術呢?如果我們使用的是大家都知道的select模型(用
fd_set
,fd_zero
, ...),用select模型的時候我們不得不迴圈檢測是否有socket的傳送接收資料的事件發生。當我們開發乙個即時通訊或者乙個遊戲伺服器的時候,乙個socket表示乙個使用者所有操作的乙個唯一標識id。因此,在伺服器上找到使用者資料,我們採用迴圈查詢或雜湊表來查詢處理套接字。當服務端使用者達到上萬的級別的時候,在服務上採用迴圈是個非常嚴重的問題。但是,如果使用的是iocp技術的話,我們就不需要迴圈。因為iocp是通過核心檢測socket事件的,並且iocp提供將socket和(即完成埠)使用者資料指標直接繫結到一起的一種機制。總而言之,iocp技術可以避免迴圈,並且在服務端更快的獲取到使用者資料。acceptex函式
使用accept(或wsaaccept)接受連線,當併發連線數超過大概30000(這取決於系統資源)的時候,容易出現wsaenobufs(10055)錯誤。這種錯誤主要是因為系統不能及時為新連線進來的客戶端分配socket資源。因此我們應該找到一種的使用之前能夠分配socket資源的方法。acceptex 就是我們尋找的答案,它的主要優勢就是在使用socket資源之前就會分分配好資源,它的其他方面的特點就比較麻煩令人費解了。(參見msdn庫。)
static memory(靜態記憶體)
在服務端應用程式使用靜態記憶體(或者叫預先分配的記憶體)是大家普遍得而且是至關重要的做法。當我們傳送或者接受資料的時候,必須使用靜態記憶體。在我得
oiocpnet
類中,我用我自己的(opreallocator)得到預分配的內。當你呼叫函式(如
writefile
,wsasend
orsend
函式)傳送大資料報(超過千位元組)的時候,接收端並沒有接受到傳送的資料報,或者說資料報中全部的資料。如果你遇到過這種情形, 那麼你可能是遇到了關於,網路硬體(路由器,集線器,等等)和緩衝---mtu(最大傳輸單元)的問題。網路硬體的最小mtu是576位元組,所以最好是大資料報切成許多小於最小mtu的小資料報。在oiocpnet,我所定義的單元的資料塊大小為buffer_unit_size(512位元組)。如果你需要更大的乙個,你可以改變它。如果你的伺服器邏輯需要很多的io操作,建立多個執行緒是個不錯的選擇。因為,當環境中需要多個io操作的時候,執行緒是有意義重大的。但是不要忘記,越多的執行緒,cpu需要做的執行緒之間的排程就會越多。如果有超過1000個執行緒,並且他們都在執行,作業系統和程序不能保持他們正常的執行狀態,因為cpu所有的精力都花費在尋找下乙個執行的執行緒,以及執行緒的排程或者說上下文切換。作為參考,oiocpnet每個cpu都會建立兩個執行緒,不在建立其他的執行緒。
oiocpnet - 關鍵所在
oiocpnet
就是應用了上述思想的乙個類。oiocpnet
的操作步驟如下:下面這個將顯示
oiocpnet的整個機制:
寫程式時需要注意的幾個關鍵點:
在上面的**片段中,ptempwritedata分配給wsasend來呼叫傳送的wsasend傳送之後立馬返回,但是
ptempwritedata必須是存在的,直到系統核心呼叫wsasend傳送完成。當傳送完成之後,釋放ptempwritedata
中的資料,釋放方式如下:
套接字的唯一性:乙個正常的socket值是唯一的。但是,作業系統分配的socket值是隨機分配的,if (0 != povl)
continue;
}}
剛關閉的socket可以重新分配給同時新連線進來的套接字。流程如下:
為新連線分配乙個值為3947的socket(作為乙個例項)邏輯伺服器使用套socket讀取資料報。
在邏輯伺服器不知情的情況下,客戶端套接字突然關閉。
乙個不同的socket被分配相同的socket值,即該socket值的復用。
伺服器邏輯將資料報寫入socket對應資料,伺服器並沒有什麼問題,但資料報可能會傳送到不同的使用者。
為了防止這種棘手的情況,oiocpnet 管理自己的套接字socketunique,obufferedsocket的乙個成員。用法:
oiocpnet
的簡單例項,如下面的**片段:int _tmain(int argc, _tchar* argv)
// _tmain()
dword winapi logicthread(void *pparam)
// process main logic.
mainlogic(piocpnet, socketunique, pbuffsock,
preaddata, readsize);
piocpnet->releasesocketevent(pslot);
} return 0;
} // logicthread()
void mainlogic(oiocpnet *piocpnet, dword socketunique,
obufferedsocket *pbuffsock, byte *preaddata, dword readsize)
// mainlogic()
我們在開始的時候設定ip位址和埠號,準備必要的資源。在邏輯執行緒中我們可以使用oiocpnet將大資料報分片成小資料報,在原來的資料的基礎上增加了4個位元組資料報長度資訊。分片和組裝操作由oiocpnet 的getsocketeventdata和writedata 兩個函式完成。因此,我們不需要關心這些。但是,你需要使用getsocketeventdata
獲取的資料報和writedata傳送資料報。在資料使用之後,使用
releasesocketevent通過pslot指標釋放(preaddata)的資料報。最後,當主線程結束後,
停止呼叫,oiocpnet釋放資源。
tcpwrite
和tcpread
兩個函式。(在nettestclient專案中的tcpfunc.h和tcpfunc.cpp)和oiocpnet進行互動,當你的客戶端需要鏈結服務端的時候。其他提示
當乙個客戶端不能產生超過5000(2023年~)連線到伺服器,檢查登錄檔。 檢查步驟包括:
執行登錄檔編輯器
開啟hkey_local_machine\system\currentcontrolset\services\tcpip\parameters
新增「maxuserport「型別為dword和設定值(最大值是65534十進位制數)。
如果你需要增加執行緒數量的測試客戶端超過2 0 xx,修改客戶端應用程式的函式堆疊大小使用編譯選項/棧:byte或createthread的引數。 在您執行測試伺服器和測試客戶端之前,設定test_ip和test_server_ip與您的伺服器的ip位址。 看到連線數量,使用效能監視器或「netstat - s」在命令提示符。
最精簡的IOCP封裝
最精簡的iocp封裝,delphi xe8直接編譯通過。winsock2.pas即使用delphi自帶的,相信xe7也能編譯,或者xe6,xe5也能。單說winsock2.pas,我見過無數種版本的了,各版本winsock 2的api的方法的引數的資料型別居然都有出入,使用不同人封裝的winsock...
axios的封裝和使用
響應 instance.interceptors.response.use res else err return promise.reject err mergeoptions options 真正傳送請求在這 request options 封裝get方法 get url,config 封裝po...
小心使用IOCP完成埠
s createsocket 假定s返回值是10 createiocompletionport s,m hcompletionport,dword ptr a,0 wsasend s,wsasend s,wsasend s,wsasend s,wsasend s,這個時候,完成埠裡累計了多條跟s相關...