IOCP的封裝和使用

2021-07-03 13:56:56 字數 4239 閱讀 7642

現在基於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,wsasendorsend函式)傳送大資料報(超過千位元組)的時候,接收端並沒有接受到傳送的資料報,或者說資料報中全部的資料。如果你遇到過這種情形, 那麼你可能是遇到了關於,網路硬體(路由器,集線器,等等)和緩衝---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

中的資料,釋放方式如下:

if (0 != povl)

continue;

}}

套接字的唯一性:乙個正常的socket值是唯一的。但是,作業系統分配的socket值是隨機分配的,

剛關閉的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位址和埠號,準備必要的資源。在邏輯執行緒中我們可以使用

getsocketeventdata

獲取的資料報和writedata傳送資料報。在資料使用之後,使用

releasesocketevent通過pslot指標釋放(preaddata)的資料報。最後,當主線程結束後,

停止呼叫,oiocpnet釋放資源。

oiocpnet將大資料報分片成小資料報,在原來的資料的基礎上增加了4個位元組資料報長度資訊。分片和組裝操作由oiocpnet 的getsocketeventdata和writedata 兩個函式完成。因此,我們不需要關心這些。但是,你需要使用tcpwritetcpread兩個函式。(在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相關...