在WINDOWS下進行網路服務端程式開發,毫無疑問,Winsock完成連接埠模型是最高效的。Winsock的完成連接埠模型藉助Widnows的重疊IO和完成連接埠來實現,完成連接埠模型懂了之後是比較簡單的,但是要想掌握Winsock完成連接埠模型,需要對WINDOWS下的線程、線程同步,WinsockAPI以及WINDOWSIO機制有一定的瞭解。如果不瞭解,推薦幾本書:《InsideWindows2000,《WINDOWS核心編程》,《WIN32多線程程式設計》、《WINDOWS網路編程技術》。在去年,我在C語言下用完成連接埠模型寫了一個WEBSERVER,前些天,我決定用C++重寫這個WEBSERVER,給這個WEBSERVER增加了一些功能,並改進完成連接埠操作方法,比如採用AcceptEx來代替accept和使用LOOKASIDELIST來管理記憶體,使得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/WSARecvIO操作,具體的IO處理過程由WINDOWS系統完成,WINDOWS系統完成實際的IO處理後,把結果送到完成連接埠上(如果有多個IO都完成了,那麼就在完成連接埠那裡排成一個隊列)。我們在另外一個線程裡從完成連接埠不斷地取出IO操作結果,然後根據需要再發出WSASend/WSARecvIO操作。
二:提高完成連接埠效率的幾種有效方法
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資料”和“單控制代碼資料”的資料結構,然後在通訊端關閉的時候釋放,這樣如果有成千上萬個客戶頻繁串連時候,會使得程式很多開銷花費在記憶體配置和釋放上。這裡我們可以使用lookasidelist。開始在微軟的platformsdk裡的SAMPLE裡看到lookasidelist,我一點不明白,MSDN裡有沒有。後來還是在DDK的文檔中找到了,,
lookasidelist
Asystem-managedqueuefromwhichentriesofafixedsizecanbeallocatedandintowhichentriescanbedeallocateddynamically.CallersoftheEx(ecutive)Supportlookasidelistroutinescanusealookasidelisttomanageanydynamicallysizedsetoffixed-sizebuffersorstructureswithcaller-determinedcontents.
Forexample,theI/OManagerusesalookasideforfastallocationanddeallocationofIRPsandMDLs.Asanotherexample,someofthesystem-suppliedSCSIclassdriversuselookasideliststoallocateandreleasememoryforSRBs.
lookasidelist
名字比較古怪(也許是我孤陋寡聞,第一次看到),其實就是一種記憶體管理方法,和記憶體池使用方法類似。我個人的理解:就是一個單鏈表。每次要分配記憶體前,先查看這個鏈表是否為空白,如果不為空白,就從這個鏈表中解下一個結點,則不需要新分配。如果為空白,再動態分配。使用完成後,把這個資料結構不釋放,而是把它插入到鏈表中去,以便下一次使用。這樣相比效率就高了很多。在我的程式中,我就使用了這種單鏈表來管理。
在我們使用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。