1. 程式人生 > >用C++實現HTTP伺服器

用C++實現HTTP伺服器

如何處理完成埠模型(IOCP)的超時問題.

作者: 闕榮文  2011/7/12


前言
完成埠(IOCP)是所有Windows I/O模型中最複雜,也是效能最好的一種.在關於IOCP的程式設計中,難點之一就是超時控制.
以下以HTTP伺服器程式為例說一說.

其實超時控制也不是很難,問題是Windows的IOCP模型本身並沒有提供關於超時的支援(也行以後的版本會有?),所以一切都要有程式設計師來完成.並且超時控制對於伺服器程式來說是必須的: HTTP伺服器程式對一個新的客戶端連線需要在完成埠上投遞一個 WSARecv() 操作,以接收客戶端的請求,如果這個客戶端連線一直不傳送資料(所謂的惡意連線)那麼投遞的這個請求永遠不會從完成埠佇列中返回,佔用了伺服器資源.如果有大量的惡意連線,服務很快就會不堪重負.所以伺服器程式必須為投遞給完成埠的請求設定一個超時時間.

那麼如何做超時控制呢?


一般有兩種思路:
1. 建立一個單獨的執行緒,每隔一段時間,輪詢一次所有的I/O請求佇列,發現有超時則取消這個I/O投遞請求.
優點:
簡單,一個執行緒,一個迴圈就可以了.

缺點:
精度和效率難以兩全.比如設定的超時時間為60秒,如果每60秒輪詢一次所有套接字,那麼有可能出現 (60 - 1 + 60)秒後才被檢測到的超時;而如果提高輪詢頻率,那麼效能又會受到影響:輪詢時,肯定要對套接字佇列加鎖.所以設定恰當的輪詢間隔是個兩難的選擇.另外,有些程式採用 min heap 最小堆演算法對輪詢進行優化可以進一步提高效率.

2. 為每一個I/O投遞請求單獨設定一個定時器.
優點:
精度高, Windows定時器大致能保證15毫秒左右的精度.

缺點:
資源消耗大,很明顯如果有大量的連線,就需要同樣數量的定時器.幸好,針對需要大量定時器的應用,Windows提供了 Timer Queue,相對於SetTimer()建立的定時器物件,用CreateTimerQueueTimer()建立的是經過優化的輕量級的物件,並且系統內部對Timer Queue也有優化,比如用執行緒池內的執行緒執行超時回撥函式等.一個程序最多可以建立多少個 TimerQueueTimer也還不清楚,我在MSDN上也沒找到相關的說明,這可能成為服務支援的最大連線數的瓶頸.(我在自己機器上(Win7 Home Basic + VS2010)測試過,第一次執行附錄3的程式碼機器幾乎失去響應,但是沒出錯.第二次加了幾個條件斷點,反正到3萬個Timer的時候,超時函式都被執行了,機器響應還很快.所以TimerQueueTimer的數量應該沒有限制或者是一個很大的數.沒權威資料,還是不確定.)

兩種方法都是可以的,具體怎麼做還是取決於程式要求.
我在設計Que's HTTP Server 時,用的是Timer Queue,根據需要,為每個socket都分配了兩個TimerQueueTimer,一個設定會話超時(即一個socket最長可以和伺服器保持多少時間的連線),一個設定為死連線超時,如果一個連線在指定的時間內,既沒有傳送資料也沒有接收資料,就會被判定為是死連線而被關閉,伺服器在每次接收或傳送資料成功時,都呼叫ChangeTimerQueueTimer()重置該定時器.只可惜條件有限,沒有在大壓力環境下測試過.只在本機上跑過幾天(極限200個左右的連線,80MB/s左右的頻寬,每秒呼叫幾百次ChangeTimerQueueTimer()重置定時器,超時誤差在8到15個毫秒左右,完全可以接受.)

HTTP伺服器程式設計中幾個需要注意的點


1. 如果一個IO請求正在處理中,則一定要確保傳人的 LPWSAOVERLAPPED 指標的有效性.這是在程式設計時無條件要保證的,否則肯定會崩潰.至於怎麼保證這點,是程式設計師的事,而不是IOCP的問題.要釋放LPWSAOVERLAPPED 指向的結構只能等到 I/O 操作從完成埠佇列返回之後才可以進行. 即只有在GetQueuedCompletionStatus()返回之後.如果在多個I/O請求中用了同一個 WSAOVERLAPPED 結構,可以設定一個引用計數,每次從GetQueuedCompletionStatus()返回計數減一,到零時可以釋放(最好避免這種設計).

2. 如何取消已經投遞的I/O請求?

答案是沒辦法取消.當然,關閉完成埠的控制代碼可以取消所有的I/O請求,但是這隻適用於程式退出時.不過,針對HTTP伺服器,關閉套接字,可以使該套接字相關的所有I/O請求都被標記為失敗,並從 GetQueuedCompletionStatus() 中返回(返回值不一定為FALSE,詳見下節)).這樣,只要在超時回撥函式中關閉對應的套接字,不釋放任何資源,完成埠服務執行緒就是從 GetQueuedCompletionStatus()返回,在確保這個套接字對應的所有I/O請求都從完成埠佇列中清除後,就可以回收資源了(主要是投遞請求時傳人的 LPWSAOVERLAPPED 指標,現在可以放心大膽的刪除了).

2011-12-09更正: 用 CancelIoEx(hSocket, NULL) 可以取消一個套接字的所有未決的I/O操作.當然,如上文說的那樣直接關閉套接字控制代碼也會導致所有未決I/O操作失敗從而達到"取消"一樣的效果.

3. GetQueuedCompletionStatus()函式返回值研究(參考MSDN),原型如下:
BOOL WINAPI GetQueuedCompletionStatus(
  __in   HANDLE CompletionPort,
  __out  LPDWORD lpNumberOfBytes,
  __out  PULONG_PTR lpCompletionKey,
  __out  LPOVERLAPPED *lpOverlapped,
  __in   DWORD dwMilliseconds
);

(1) 如果I/O操作(WSASend() / WSARecv())成功完成,那麼返回值為TRUE,並且 lpNumberOfBytes 為已傳送的位元組數.注意,已傳送的位元組數有可能小於你請求傳送/接收的位元組數.
(2) 如果對方關閉了套接字,那麼有兩種情況
(a) I/O操作已經完成了一部分,比如WSASend()請求傳送1K位元組,並且其中的512位元組已經發送完成,則返回值為TRUE, lpNumberOfBytes 指向的值為512, lpOverlapped 有效.
(b) I/O操作沒有完成,那麼返回值為FALSE, lpNumberOfBytes 指向的值為0, lpCompletionKey, lpOverlapped 有效.
(3) 如果我們程式這方主動關閉了套接字,則和(2)的情況一樣,沒有區別.
(4) 如果發生了其它錯誤,則返回值為FALSE,並且 lpCompletionKey, lpOverlapped = NULL,在這種情況下,應該呼叫 GetLastError() 檢視錯誤資訊,並且退出等待

GetQueuedCompletionStatus()的迴圈.

4. 每次呼叫網路函式(WSARecv(), WSASend()等)都要檢查返回值,並作相應處理.網路事件相當複雜,什麼情況都有可能出現,只有檢測每個函式的返回值,程式才會健壯.

後記
我在學習IOCP的過程中,在網上搜看了很多相關的文章,帖子,挑兩篇附在後面,感謝原作者.


附錄:1. http://club.itqun.net/showtopic-82514.html
帖子中網友 WinEggDrop 第36樓說的非常清楚,贊同

---------------------------------------------
最後一貼關於這個討論的,主要是說下我說過的幾種方法.頂樓所說的,主要就是一種超時檢測機制,很多伺服器程式都需要這樣的機制,因為太多空閒的連線還是使用一定量的系統資源的,有些伺服器,象FTP伺服器,有時還限制了最大登陸的連線數,萬一有人惡意大量地連線,但這些連線不被系統定時斷開的話,那麼正常的使用者有可能無法登陸FTP伺服器(因為連線數到達上限)

1.使用setsockopt設定SO_RCVTIMEO
這種方法簡單好用,但缺點是隻用於阻塞的socket,而且有時因為對方的非正常斷開而無法檢測到.

2.在接收資料前使用select(),select()返回可讀才呼叫recv()等API.
這種方法一樣簡單好用,但缺點還是主要適用於阻塞socket,一般非阻塞socket也可用,只不過要呼叫個死迴圈不斷地檢測select()返回值,很是浪費資源.

3.定時掃描所有客戶socket的方法(樓主正採用的方法).這方法就是記錄每次每個socket資料通訊時的時間,然後在掃描時再和當前時間比較,如果時間差高於超時機制的限制時間,

就將socket斷開.
這種方法使用起來也是很簡單的,只要建一個執行緒定時地掃描所有客戶socket列表.適用性很強,所有socket模式都可相容的.需要注意的是這方法臨界要做好,不然是挺容易出現問題(在掃描期間有socket正常的斷開時資源被釋放時,掃描列表時如果沒做臨界,那麼掃描時就很有可能訪問了非法的記憶體).這方法有個缺點就是超時機制的誤差比較高,因為如果超時檢測的時間設定為N,那麼是有可能出現N-1秒的誤差的.設定檢測的時間越長,出現的誤差時間就越長.由於每次都要掃描所有的客戶socket列表,如果socket比較多時,設定這個檢測時間就是個"雞肋".檢測時間設定得過短,頻煩的掃描對系統資源和程式效能必然多少有是影響;而設定時間過長,又令誤差時間過大.

4.使用系統的Timer
標準的Timer:使用SetTimer()設定Timer,使用KillTimer()刪除Timer.優點是適用於所有系統,也適用於所有socket模型.缺點是精確度不高,而且是訊息機制的,如果太多訊息要處理,Timer觸發時間會被延遲.NT系統核心Timer.優點是精確度高,缺點是隻能用於NT系統.

所有上面的方法我都在以前寫的伺服器程式中嘗試過,最終我是選用了NT系統核心Timer那種方法.這種方法是不是最高效的,我也不清楚,只是我自己傾向於這方法,自認為是比較高效的方法(事實上是不是高效的,我也無法測試).
---------------------------------------------

附錄2. http://blog.sina.com.cn/s/blog_62b4e3ff0100nu84.html
學習筆記:神祕的 IOCP 完成埠
(2010-12-19 15:53:36)
轉載
標籤:
it
    
【什麼是IOCP】
是WINDOWS系統的一個核心物件。通過此物件,應用程式可以獲得非同步IO的完成通知。
這裡有幾個角色:
角色1:非同步IO請求者執行緒。簡單的說,就是呼叫WSAxxx()函式(例如函式WSARecv,WSASend)的某個執行緒。
       由於是“非同步”的,當角色1執行緒看到WSAxxx()函式返回時,它並不能知道本次IO是否真的完成了。
       注:當WSAxxx返回成功true時,實際已經讀到或傳送完資料了(同步的獲得IO結果了)。
       為了統一邏輯,我們還是要放到角色2執行緒中,統一處理IO結果。
       
角色2:非同步IO完成事件處理執行緒。簡單的說,就是呼叫GetQueuedCompletionStatus函式的執行緒。
       角色1投遞的某個非同步IO請求M,角色2執行緒一定能獲得M的處理結果(無非是IO成功或失敗)       
角色3:作業系統。負責角色1和角色2的溝通。OS接收角色1的所有非同步IO請求。
       OS處理(實際的IO讀寫)排隊的很多非同步IO請求。OS的程式設計師是很牛的,他們能最大化利用CPU和網路。
       OS把所有IO結果放入{IOCP完成佇列C}中。
       OS能排程角色2執行緒的執行和睡眠,能控制角色2執行緒同時執行的執行緒個數。
       角色2通過GetQueuedCompletionStatus函式,讀取到{IOCP完成佇列C}中完成的IO請求。

【需要建立幾個角色2執行緒呢】
CreateIoCompletionPort()函式建立一個完成埠,其中有一個引數是NumberOfConcurrentThreads。
這個引數的含義是:程式設計師期望的同時執行的角色2執行緒數。0代表預設為本機器的CPU個數。
程式設計師可以建立任意數量的角色2執行緒。
例如:NumberOfConcurrentThreads設定為2,而實際建立6個角色2執行緒,或100個,或0個。

如何理解這兩個數的差異呢?
OS努力維持NumberOfConcurrentThreads個執行緒併發的執行,即使我建立100個角色2執行緒。
如果{IOCP完成佇列C}中排隊等待處理的{IO結果項}很少,角色2執行緒能很快處理完,則實際可能只有1個角色2執行緒在工作,其他執行緒都在睡眠(即使NumberOfConcurrentThreads設定成100,也只有一個執行緒在工作)。
如果{IOCP完成佇列C}中排隊等待處理的{IO結果項}很多,角色2執行緒處理需要很多CPU時間,則實際可能會有很多角色2執行緒會被喚醒工作。當然前提是我實際建立了很多角色2執行緒。極端情況下,如果角色2執行緒都退出了,則{IOCP完成佇列C}可能會被擠爆了。

為什麼一般情況下,NumberOfConcurrentThreads設定為2,而實際建立6個角色2執行緒呢?
考慮到我們的角色2執行緒不只是CPU計算,它還可能去讀寫日誌檔案,呼叫Sleep,或訪問某個Mutex物件(造成執行緒被排程為睡眠)。這樣,OS會啟用一些“後備軍”角色2執行緒去處理{IOCP完成佇列C}。所以實際建立6個角色2執行緒,有幾個可能是後備軍執行緒。如果我們的角色2執行緒是純CPU密集計算型的(可能有少量的臨界區訪問,也不會輕易放棄CPU控制權),那麼我們只需要實際建立角色2執行緒數=CPU個數,多建立了也沒益處(但也沒壞處,可能OS讓他們一直都睡眠,做後備軍)。

【非同步讀寫如何控制位元組數】

或曰,某個WSASend呼叫,在網路正常的情況下,{實際傳送位元組數}(簡稱T)就是{需要傳送的位元組數}(簡稱R)。我試驗了一下,從1M的buff,2M的buff...當開到很大的buff時,終於出現T<R的時候。
如果我們的應用需要一次傳送很大量的資料時,應該檢查T是否小於R。當傳送的位元組數不足時,應該繼續傳送剩餘的(未傳送出去的)部分。

對於WSARecv接收資料,應接收多大的位元組數呢?假如應用層協議規定,我們的資料長度不是固定的,這就是一個很棘手的問題。一般情況下,應用層協議規定,一段邏輯上是一組的資料,分包頭部分和包體部分。包頭是固定長度的,包體是變長的。包頭含有如下資訊:包體的長度位元組數。我們先收一個固定長度的包頭,從中解析出“包體長度資訊”,然後我們再次發出一個WSARecv收包體。我稱作這個方法為“包頭包體兩階段接收法”。

【非同步讀寫如何控制超時】

假如我們接受一個數據包,發出WSARecv{非同步IO: X}。這個{非同步IO: X}可能長時間無法獲得結果。假如對方客戶端惡意的不傳送任何資料。IOCP本身機制不提供任何超時控制。只能我們程式設計師控制這個超時。我們發出一個WSARecv呼叫後,通過維護某種{資料結構: D},記住此時的時間。在未來的某個時間我們的程式要檢查這個{資料結構: D}, 判斷這個WSARecv呼叫是否有結果了。當然此{資料結構: D}的狀態改變由{角色2執行緒}負責。
如果{角色2執行緒}通過GetQueuedCompletionStatus呼叫獲得了{非同步IO: X}的結果,則改變{資料結構: D}的狀態。我們只要判斷{資料結構: D}的某個狀態未改變,則一定是這個{非同步IO: X}未被完成(客戶端沒有傳送任何資料)。

控制超時和控制位元組數往往有關聯。假如惡意的客戶端只發送部分位元組數,我們還要處理這種情況。
假如協議要求100個位元組,客戶端一次傳來10個,我們可以毫不客氣的幹掉這個客戶端。這個策略比較狠了些。我們需要溫和一點的策略。可能因為網路原因,剩下的90個位元組很快就能到來,我們可以繼續在規定時間等接受剩餘的90個位元組。如果超時了,才把這個客戶端幹掉。

【IOCP系統資源耗盡的問題】

假如我們有10000個客戶端socket連線,為了接收他們傳送過來的資料,我們需要預先投遞10000個WSARecv。
假如每個非同步讀需要應用層程式設計師提供10k的緩衝區,則一共需要的使用者緩衝區為 10000*10k=97M 記憶體。windows要求這97M資料必須被OS“鎖定”,意思大體是需要佔用大量的OS的資源了。所以程式很可能會因為10000個客戶同時連線,而耗盡資源。WSAENOBUF錯誤同此有關。
解決方法是投遞0位元組數請求的WSARecv。虛擬碼如下:

WSABUF DataBuf;
DataBuf.len=0;
DataBuf.buf=0;
WSARecv(socket, &DataBuf, 1,...);
當有資料到來時,這個非同步IO會從角色2執行緒中得到結果。由於它是0位元組的讀,所以它沒有觸碰任何socket緩衝區的到來的任何資料。我們付出很小的成本(大約每個連線節省了10k)就能知道哪個客戶端的資料到來了。別小看了每個連線節省了這麼點資源,連線數大了節約的總量就很可觀了。如果客戶端數量很少,這個技巧就沒什麼意思了。

【優雅的殺死角色2執行緒】

PostQueuedCompletionStatus函式會向{IOCP完成佇列C}中push進去一條記錄。這樣角色2執行緒就能獲得這個“虛偽或模擬”的非同步IO完成事件。為什麼要“假冒”一條{IOCP完成佇列C}的條目呢?用處嗎,程式設計師自己去想吧(意思是用處多多了)。一般來說,我們用它“優雅的殺死角色2執行緒”。虛擬碼如下:

typedef struct
{
   OVERLAPPED Overlapped;
   OP_CODE op_type;
   ...
} PER_IO_DATA;
PER_IO_DATA* PerIOData = ...
PerIOData->op_type = OP_KILL; //操作型別是殺死執行緒
PostQueuedCompletionStatus(...PerIOData...);
//如果有N個角色2執行緒,則需要呼叫N次,這樣{IOCP完成佇列C}中才能有N個這個的條目。

角色2執行緒:
PER_IO_DATA* PerIOData=0;
GetQueuedCompletionStatus(...&PerIOData...);
if (PerIOData->op_type == OP_KILL){  return ; } //從執行緒中自然return,就是優雅的退出執行緒。

【大頭的錯誤處理】

GetQueuedCompletionStatus函式的錯誤處理比較複雜。

1 如果GetQueuedCompletionStatus返回false:
1.1 如果Overlapped指標非空
    恭喜你,你投遞的非同步IO獲得結果了,只不過是失敗的結果。好孬也終於回來個信兒了。
    這可能是socket連線斷了等等。
    1.1.1 如果GetLastError獲得的錯誤號為ERROR_OPERATION_ABORTED
          一定是有東西呼叫了CancelIO(socket)了。所有同這個socket相關的非同步IO請求都會被取消。
    1.1.2 如果GetLastError 獲得的錯誤號為其他的東西
          可能是IO沒成功,如socket連線斷開了等等。
1.2 如果Overlapped指標空
    這可不是好訊息,因為這意味著IOCP本身有重大故障了。比如我們意外的把IOCP的控制代碼CloseHandle了。
    1.2.1 如果GetLastError獲得的錯誤號為WAIT_TIMEOUT
          可能GetQueuedCompletionStatus設定的超時引數dwMilliseconds不是INFINITE。我們繼續呼叫GetQueuedCompletionStatus重新等待吧。
    1.2.1 如果GetLastError獲得的錯誤號ERROR_ABANDONED_WAIT_0, 或者其他
          IOCP本身都完蛋了,角色2執行緒應另找東家了,或者就地自我了斷算了。
2 如果GetQueuedCompletionStatus返回true:
  恭喜你,非同步IO成功了。
  通過lpNumberOfBytes, lpCompletionKey, and lpOverlapped這三個引數獲得詳細資訊。
  lpNumberOfBytes:實際傳輸的位元組數。(可能比需要傳輸的位元組數少)
  lpCompletionKey:這就是著名的PerHandleData,可以知道這是哪個socket連線的。
  lpOverlapped:   這就是著名的PER_IO_DATA, 同某次非同步IO呼叫關聯,
        比如某次WSASend(Overlapped引數=0x123)呼叫,這裡能重新拿到lpOverlapped==0x123。
我們可以根據這個指標,得知這個IO結果是對應著哪次WSASend()呼叫的結果。         

我滿以為這個錯誤處理天衣無縫,直到有一次測試。我對一個socke投遞了100個WSARecv。當我故意把客戶端關閉後,這些非同步IO不出意外的都在角色2執行緒的GetQueuedCompletionStatus函式處獲得結果了。令我吃驚的是,GetQueuedCompletionStatus返回為TRUE!!!,並且GetLastError()返回值是0!!!
令我欣慰的是lpNumberOfBytes值為0(否則真見鬼了)。所以看到GetQueuedCompletionStatus返回true,不要高興的太早了。

2.1 把lpOverlapped指標解釋成PER_IO_DATA資料結構。如果PerIOData->op_type == OP_KILL,可能這個是PostQueuedCompletionStatus偽造的一個IO完成事件。
2.2 判斷是否(lpNumberOfBytes==0)。如果這個IO結果的確是某個WSAxxx()的結果,而不是PostQueuedCompletionStatus偽造的,則這個IO對應的socket可能斷了。
2.3 (lpNumberOfBytes>0) ,這才是真正的IO完成的事件呢。可能99.9%的機會,分支跑到這裡的。
 
【在同一個socket上一次投遞多個非同步IO】
一次投遞多個WSASend(1234,&Buff1,...); WSASend(1234,&Buff2,...); ... 好像沒問題。
如果一次投遞多個WSARecv(1234,&Buff1,...);WSARecv(1234,&Buff2,...);好像有些需要闡明的問題。

第一:Windows保證按照你投遞WSARecv的順序,把網路上到達的資料按先後順序放入Buff1,Buff2。
      如果網路上到來的資料為 AAAAUUUU, 假設Buff1長度4,Buff2長度4,
      則保證Buff1獲得AAAA,Buff2獲得UUUU
第二:如果有多個角色2執行緒,可能由於執行緒排程的“競爭條件race condition”,
      某執行緒首先執行Buff2的完成處理過程。
      如果我在角色2執行緒中,打印出收到的資料,可能打印出如下結果:UUUUAAAA。這絕不是違反了TCP協議,       而是多執行緒的問題。其實解決方案很簡單。說者費事,上虛擬碼
typedef struct
{
   OVERLAPPED Overlapped;
   ...
   int Package_Number; //我對每一次IO,夾帶本次呼叫順序號
   ...
} PER_IO_DATA;

PER_IO_DATA* PerIOData1=...
PerIOData1->Package_Number = 1 ; //第一次呼叫
WSARecv(1234, &Buff1,...PerIOData1...);

PER_IO_DATA* PerIOData2=...
PerIOData1->Package_Number = 2 ; //第二次呼叫
WSARecv(1234, &Buff2,...PerIOData2...);

我們需要維護某種資料結構,記住我們發出了兩個WSARecv。
當收到IO結果後,程式需要判斷,只有1,2兩個呼叫都從角色2執行緒獲得結果後,才能按順序把Buff1和Buff2拼接,就是符合順序的AAAAUUUU。當然,還有其他更好的方式,這裡只展示基本原理。

第三:真有必對同一個socket一次投遞多個WSARecv嗎?
      這個問題同【IOCP系統資源耗盡的問題】,不矛盾。我們假設在投遞多個WSARecv時,已經預見到網路上將到來某個socket的大量資料。 根據網路資料介紹,這樣可以充分發揮多CPU併發運算的能力。我想在雙核CPU機器上,一個CPU處理Buff1,同時另一個CPU處理Buff2。
      如果是少量客戶端連線,每個連線可能突然發生大量資料的傳送,這個做法可能能加快從Socket緩衝區拷貝資料到應用程式Buff的速度(個人揣測)。
      如果是大量客戶端(10000)連線,每個連線傳送的資料量很少,這個做法我個人認為沒什麼意義。我想CPU數量就2個,不會輕易就閒下來吧?
      有一個重要原因,需要投遞多個buffer給windows。假如我預計到某個socket一次傳過來2M的資料,而
我沒有2M大小的buffer,我只有1M大小的buffer。我需要先呼叫一次WSARecv,等待收完這1M資料後,再發一個
WSARecv。或者我用其他方法,提供給windows系統2個1M的buff。

第四:假設我們真需要一次投遞多個Buff,接收資料,有必要用多次WSARecv呼叫嗎?
      這裡有個可能的替代做法,上虛擬碼:
      char *raw1 = new char[BUFF_SIZE];
      WSABUF[2] wsabuf;
      wsabuf[0].buf = raw1 ;
      wsabuf[0].len = BUFF_SIZE;

      char *raw2 = new char[BUFF_SIZE];
      wsabuf[1].buf = raw2 ;
      wsabuf[1].len = BUFF_SIZE;

      WSARecv(1234, &wsabuf, 2 ... );  
      //重點在引數2上,指示了WSABUF結構體的個數是2個。一般大量IOCP的例子裡這個引數都是1
      
      這個方法我認為更簡單,不知道是我自己“2”還是網上的其他人“2”,一次發出多個WSARecv,把這些分散的IO收集起來也是費事的事。UNIX系統的scatter-gather IO類似於這個機制。

-----------
附錄3

long g_nCalled = 0;
VOID CALLBACK TimerCallback(PVOID lpParameter, BOOLEAN TimerOrWaitFired)
{
	InterlockedIncrement(&g_nCalled);
}


void CreateAsManyTimerAsPossbile()
{
	// 建立儘可能多的TimerQueueTimer
	CString strMessage(_T(""));
	HANDLE hTimerQueue = CreateTimerQueue();
	if(NULL == hTimerQueue)
	{
		strMessage.Format(_T("Unable to create timer queue, error code:%d."), GetLastError());
	}
	else
	{
		int nTimerCount = 0;
		while(1)
		{
			HANDLE hTimer = NULL;
			if( !CreateTimerQueueTimer(&hTimer, hTimerQueue, TimerCallback, NULL, 100, 0, 0) )
			{
				strMessage.Format(_T("Failed to create timer queue timer, current timer count:%d, timer callback called:%d, error code:%d."), 

nTimerCount, g_nCalled, GetLastError());
				break;
			}
			if(++nTimerCount >= 5000)
			{
				//ASSERT(0);
			}
		}
		DeleteTimerQueueEx(hTimerQueue, NULL);
	}

	AfxMessageBox(strMessage);
}