在WINDOWS下進行網絡服務端程序開發,毫無疑問,Winsock 完成端口模型是最高效的。Winsock的完成端口模型借助Widnows的重疊IO和完成端口來實現,完成端口模型懂了之後是比較簡單的,但是要想掌握Winsock完成端口模型,需要對WINDOWS下的線程、線程同步,Winsock
API以及WINDOWS IO機制有一定的了解。如果不了解,推薦幾本書:《Inside Windows 2000,《WINDOWS核心編程》,《WIN32多線程程序設計》、《WINDOWS網絡編程技術》。在去年,我在C語言下用完成端口模型寫了一個WEBSERVER,前些天,我決定用C++重寫這個WEBSERVER,給這個WEBSERVER增加了一些功能,並改進完成端口操作方法,比如采用AcceptEx來代替accept和使用LOOKASIDE
LIST來管理內存,使得WEBSERVER的性能有了比較大的提高。
在重寫的開始,我決定把完成端口模型封裝成一個比較通用的C++類,針對各種網絡服務端程序的開發,只要簡單地繼承這個類,改寫其中兩個虛擬函數就能滿足各種需要。到昨天為止,WEBSERVER重寫完畢,我就寫了這篇文章對完成端口模型做一個總結,並介紹一下我的這個類。
一:完成端口模型
至於完成端口和Winsock完成端口模型的詳細介紹,請參見我上面介紹的那幾本書,這裡只是我個人對完成端口模型理解的一點心得。
首先我們要抽象出一個完成端口大概的處理流程:
1:創建一個完成端口。
2:創建一個線程A。
3:A線程循環調用GetQueuedCompletionStatus()函數來得到IO操作結果,這個函數是個阻塞函數。
4:主線程循環裡調用accept等待客戶端連接上來。
5:主線程裡accept返回新連接建立以後,把這個新的套接字句柄用CreateIoCompletionPort關聯到完成端口,然後發出一個異步的WSASend或者WSARecv調用,因為是異步函數,WSASend/WSARecv會馬上返回,實際的發送或者接收數據的操作由WINDOWS系統去做。
6:主線程繼續下一次循環,阻塞在accept這裡等待客戶端連接。
7:WINDOWS系統完成WSASend或者WSArecv的操作,把結果發到完成端口。
8:A線程裡的GetQueuedCompletionStatus()馬上返回,並從完成端口取得剛完成的WSASend/WSARecv的結果。
9:在A線程裡對這些數據進行處理(如果處理過程很耗時,需要新開線程處理),然後接著發出WSASend/WSARecv,並繼續下一次循環阻塞在GetQueuedCompletionStatus()這裡。
具體的流程請看附圖,其中紅線表示是WINDOWS系統進行的處理,不需要我們程序干預。
歸根到底概括完成端口模型一句話:
我們不停地發出異步的WSASend/WSARecv IO操作,具體的IO處理過程由WINDOWS系統完成,WINDOWS系統完成實際的IO處理後,把結果送到完成端口上(如果有多個IO都完成了,那麼就在完成端口那裡排成一個隊列)。我們在另外一個線程裡從完成端口不斷地取出IO操作結果,然後根據需要再發出WSASend/WSARecv
IO操作。
二:提高完成端口效率的幾種有效方法
1:使用AcceptEx代替accept。AcceptEx函數是微軟的Winsosk 擴展函數,這個函數和accept的區別就是:accept是阻塞的,一直要到有客戶端連接上來後accept才返回,而AcceptEx是異步的,直接就返回了,所以我們利用AcceptEx可以發出多個AcceptEx調用
等待客戶端連接。另外,如果我們可以預見到客戶端一連接上來後就會發送數據(比如WEBSERVER的客戶端浏覽器),那麼可以隨著AcceptEx投遞一個BUFFER進去,這樣連接一建立成功,就可以接收客戶端發出的數據到BUFFER裡,這樣使用的話,一次AcceptEx調用相當於accpet和recv的一次連續調用。同時,微軟的幾個擴展函數針對操作系統優化過,效率優於WINSOCK
的標准API函數。
2:在套接字上使用SO_RCVBUF和SO_SNDBUF選項來關閉系統緩沖區。這個方法見仁見智,詳細的介紹可以參考《WINDOWS核心編程》第9章。這裡不做詳細介紹,我封裝的類中也沒有使用這個方法。
3:內存分配方法。因為每次為一個新建立的套接字都要動態分配一個“單IO數據”和“單句柄數據”的數據結構,然後在套接字關閉的時候釋放,這樣如果有成千上萬個客戶頻繁連接時候,會使得程序很多開銷花費在內存分配和釋放上。這裡我們可以使用lookaside
list。開始在微軟的platform sdk裡的SAMPLE裡看到lookaside list,我一點不明白,MSDN裡有沒有。後來還是在DDK的文檔中找到了,,
lookaside list
A system-managed queue from which entries of a fixed size can be allocated
and into which entries can be deallocated dynamically. Callers of the
Ex(ecutive) Support lookaside list routines can use a lookaside list
to manage any dynamically sized set of fixed-size buffers or structures
with caller-determined contents.
For example, the I/O Manager uses a lookaside for fast allocation
and deallocation of IRPs and MDLs. As another example, some of the system-supplied
SCSI class drivers use lookaside lists to allocate and release memory
for SRBs.
lookaside list名字比較古怪(也許是我孤陋寡聞,第一次看到),其實就是一種內存管理方法,和內存池使用方法類似。我個人的理解:就是一個單鏈表。每次要分配內存前,先查看這個鏈表是否為空,如果不為空,就從這個鏈表中解下一個結點,則不需要新分配。如果為空,再動態分配。使用完成後,把這個數據結構不釋放,而是把它插入到鏈表中去,以便下一次使用。這樣相比效率就高了很多。在我的程序中,我就使用了這種單鏈表來管理。
在我們使用AcceptEx並隨著AcceptEx投遞一個BUFFER後會帶來一個副作用:比如某個客戶端只執行一個connect操作,並不執行send操作,那麼AcceptEx這個請求不會完成,相應的,我們用GetQueuedCompletionStatus在完成端口中得不到操作結果,這樣,如果有很多個這樣的連接,對程序性能會造成巨大的影響,我們需要用一種方法來定時檢測,當某個連接已經建立並且連接時間超過我們規定的時間而且沒有收發過數據,那麼我們就把它關閉。檢測連接時間可以用SO_CONNECT_TIME來調用getsockopt得到。
還有一個值得注意的地方:就是我們不能一下子發出很多AcceptEx調用等待客戶連接,這樣對程序的性能有影響,同時,在我們發出的AcceptEx調用耗盡的時候需要新增加AcceptEx調用,我們可以把FD_ACCEPT事件和一個EVENT關聯起來,然後用WaitForSingleObject等待這個Event,當已經發出AccpetEx調用數目耗盡而又有新的客戶端需要連接上來,FD_ACCEPT事件將被觸發,EVENT變為已傳信狀態,
WaitForSingleObject返回,我們就重新發出足夠的AcceptEx調用。
關於完成端口模型就介紹到這裡。下面介紹我封裝的類,這個類寫完後,我用這個類做了個ECHOSERVER。
void main()
{
CompletionPortModel p;
p.Init();
p.AllocEventMessage();
if (FALSE == p.PostAcceptEx())
{
return;
}
p.ThreadLoop();
return;
}
我在我自己的機器上測試,
客戶端的代碼是
for (int i=0; i<10000; i++)
{
SOCKET s = socket(….);
connect(….);
send(…);
recv(…..)
cout << buffer << endl;
}
結果客戶端程序在循環到3000多次的時候死掉,但是服務端程序運行良好,重新啟動客戶端程序,發送接收數據正常。
使用的時候,只需要從這個類派生一個子類,並改寫HandleData和DataAction這兩個虛函數,對於那些需要連續發送相關聯的數據應用(比如傳送文件),使用者需要自己擴展這兩個函數,比如創建一個全局隊列,每次從完成端口裡得到數據後插入隊列,然後用另外一個線程專門處理這個隊列。。。
從結果來看,這個類還有不少需要改進的地方,比如沒考慮多處理器上運行的情況。沒有考慮完成端口線程阻塞情況,如果考慮完成端口阻塞情況,那麼應該創建CPU數據*2個完成端口線程等等,,因為我同時正在做的畢業設計NDIS驅動防火牆開發正在一個比較難的地方卡住了,時間和精力有限,就沒有對這個類進行進一步完善,程序中也許有不合理和錯誤的地方,請高手多多指教。對於高性能的服務端程序開發是比較難的,記得有次和騰訊一個技術人員聊天,他說,像騰訊QQ的開發,難點不在客戶端,而在服務端各個服務器之間的通信和同步。服務端程程序的集群和負載平衡是一個很復雜的問題,我在這方面剛接觸,希望能有更多的高手出來共享自己的經驗。
封裝這個類的時候,我把最新的PLATFORM SDK裡的例子看了一遍,借鑒了其中很多思路和方法,在此對寫這個例子的微軟程序員表示感謝:)
DEMO就是一個ECHOSERVER,記得使用Release模式編譯。