| 
  鳴謝 感謝PiggyXP兄的雄文《手把手叫你玩轉(zhuǎn)網(wǎng)絡(luò)編程系列之三——完成端口(Completion Port)詳解》提供的思路
 目錄C++11標(biāo)準(zhǔn)提出來有些年頭了,十一放假沒事研究了一下IOCP,想著能不能用C++11實現(xiàn)一個高性能的服務(wù)器。當(dāng)然,目前有許多十分成熟的C++網(wǎng)絡(luò)庫,比如ACE,asio等等。但是如果想深入了解其本質(zhì),在Windows平臺下就必須了解Socket結(jié)合IOCP的使用原理。 本文盡可能把筆者在使用C++11實現(xiàn)IOCP服務(wù)器的過程中遇到的困難和問題展現(xiàn)給大家,讓大家學(xué)習(xí)起來少走些彎路。由于代碼比較底層,所以有些細(xì)節(jié)希望大家在看本文和代碼的時候能夠揣摩和理解。本文假定讀者總體把握了PiggyXP原文的相關(guān)內(nèi)容并具有相當(dāng)?shù)腤indow編程的相關(guān)知識(熟悉WinSock2庫基本函數(shù)的使用,Windows多線程的基本概念等)、C++11/03編程基礎(chǔ)(STL,仿函數(shù)等)。 在每一節(jié)標(biāo)題后都有箭頭指向目錄,文檔某些位置可能會有返回箭頭(返回到可能你在閱讀的地方),希望能幫助大家更好的理解本文。 本文代碼遵循Apache License 2.0協(xié)議,歡迎各位大神拍磚。分享帶來進(jìn)步,如需轉(zhuǎn)載請標(biāo)明作者和出處,謝謝! 
  溫馨提示:由于筆者水平有限,雖經(jīng)過仔細(xì)調(diào)試,但本文代碼仍然可能存在筆者未知的Bug或者性能缺陷。請大家發(fā)現(xiàn)問題后能夠及時聯(lián)系我,讓我們共同進(jìn)步。 
 開發(fā)環(huán)境↑表1 開發(fā)環(huán)境
  | 軟件/系統(tǒng) | 版本 |  
  | 操作系統(tǒng) | Windows 10 v1607 x64 |  
  | IDE/編譯器 | Visual Studio 2015/CL 19 |  
  | Win SDK | 10.0.10240 |  
  | 編程語言 | C++11 |  
 IOCP相關(guān)知識↑
  本節(jié)參考文獻(xiàn)  Nasarre C, Richter J. Windows? via C/C++[M].  Pearson Education, 2007: 291-316.
 引入在生活中,異步的概念是很常見的。比如你洗衣服時突然女朋友(程序員有女朋友?)來了,你從洗衣間出去招待,而洗衣機(jī)則按照你的指令繼續(xù)在工作。當(dāng)你招呼完女朋友回到洗衣間的時候,衣服已經(jīng)洗好了。也就是在女朋友來的時間點,你與洗衣機(jī)分離,它按照你的指令在完成工作,而你卻可以處理其他更需要處理的事情。當(dāng)你處理完回來后,洗衣機(jī)可能早已經(jīng)完成了它的工作,你只需要將衣服取出晾起來就可以了。而同步就是你家沒有洗衣機(jī),當(dāng)女朋友來的時候要么中斷洗衣服去招待女朋友,要么讓女朋友等待自己把衣服洗完,一件事情只能在另一件事情之后發(fā)生。這樣,大家就能明顯看出來有臺洗衣機(jī)的好處了。 不過如何知道衣服洗完了呢?Windows牌洗衣機(jī)給我們提供了這么四種方式: 表2 Windows 提供的4種異步方式
 
  | 方式 | 解釋 | 相關(guān)技術(shù) |  
  | LED燈 | 洗完一件衣服就亮燈,但只有一個燈,其他人可以幫忙處理 | 觸發(fā)設(shè)備內(nèi)核對象 |  
  | 高級LED燈 | 洗完一件衣服就亮燈,可以有多個燈,其他人可以幫忙處理 | 觸發(fā)事件內(nèi)核對象 |  
  | 發(fā)送短信 | 洗完一件衣服就發(fā)送一條短信,有一個短信列表,但只有你能夠處理 | 可提醒IO(APC) |  
  | 群發(fā)短信 | 洗完一件衣服就發(fā)送一條短信,有一個短信列表,其他人可以幫忙處理 | IO完成端口(IOCP) |  這樣,大家就很明白IOCP的好處了:不需要去時刻看著燈亮不亮;短信到了可以去處理也可以不去處理;不僅你能處理,還有家人也能幫你處理。 觸發(fā)設(shè)備內(nèi)核對象、觸發(fā)事件內(nèi)核對象和可提醒IO就不展開討論了,有興趣的朋友可以查閱本節(jié)列出的參考文獻(xiàn),下面進(jìn)入正題。 IOCP狀態(tài)機(jī)這一小節(jié)可能比較難,希望大家能夠耐心看下去,因為要真正掌握IOCP就必須弄清楚它內(nèi)在的原理。先給出IOCP的狀態(tài)機(jī),如圖1所示: 
   圖1 IOCP狀態(tài)機(jī)
 
 下面給出圖中各組件的相關(guān)說明: 表3 IOCP相關(guān)組件說明
 
  | 組件 | 簡要解釋 |  
  | 等待隊列 | 當(dāng)線程池中的某線程在等待IO操作時(調(diào)用 GetQueuedCompletionStatus函數(shù)),IOCP將線程加入等待隊列。IOCP在IO操作完成后將返回結(jié)果加入完成隊列,由等待隊列中的最后一個加入的線程處理。
 |  
  | 已釋放列表 | 當(dāng)?shù)却木€程處理完IO操作后或是從暫停狀態(tài)被喚醒都會加入此列表。 當(dāng)線程再次調(diào)用
 GetQueuedCompletionStatus函數(shù)將使自己再次加入等待隊列;將自身掛起將加入已暫停列表。 |  
  | 已暫停列表 | 當(dāng)已釋放列表中的線程掛起時將加入已暫停列表;當(dāng)掛起線程被激活時線程加入已釋放列表。 |  
  | 完成隊列 | IOCP完成指定IO操作后將執(zhí)行結(jié)果插入完成隊列。這個隊列時先進(jìn)先出的。 |  
  | IOCP設(shè)備列表 | 即要進(jìn)行異步IO操作的設(shè)備列表(可以是文件,也可以是套接字),所有的IO操作都圍繞這些設(shè)備進(jìn)行。 |   這樣,整個IOCP服務(wù)器創(chuàng)建的流程就很明了了:?
 
  
  創(chuàng)建一個新的完成端口,處理所有的IO請求。創(chuàng)建一個線程池,此時線程處于已釋放列表。創(chuàng)建一個Socket并將其綁定在創(chuàng)建的完成端口上,作為IO操作的實體。利用這個套接字進(jìn)行Listen操作,并向第1步創(chuàng)建的完成端口中投遞Accept消息,將第2步創(chuàng)建線程置于等待隊列中等待客戶端連接。當(dāng)客戶端連接后,IOCP將在IO完成隊列插入Accept,等待隊列中的線程將得到Accept,并創(chuàng)建新的Socket作為與客戶端通信的套接字,并將其綁定在第1步創(chuàng)建好的完成端口上。此后,無論是Recv,Send都照此步驟進(jìn)行即可。 這里有幾個細(xì)節(jié)需要注意: 
  1. 最合適的線程數(shù)應(yīng)當(dāng)是多于處理器核心數(shù)的 多線程優(yōu)化理論告誡我們,為了避免ring0與ring3之間的上下文切換,我們應(yīng)當(dāng)將線程數(shù)設(shè)置為處理器核數(shù)。但是微軟在設(shè)計IOCP的時候想到了這樣一個問題:考慮到線程掛起,如果按照理論值設(shè)置線程數(shù),將有可能出現(xiàn)實際工作線程數(shù)小于CPU所能接受的最大工作線程數(shù),這樣就無法有效發(fā)揮多線程的優(yōu)勢。因此,最理想的線程數(shù)量應(yīng)當(dāng)多于處理器核心數(shù)的,經(jīng)驗值為兩倍核心數(shù)。 
  2. 等待隊列是后入先出的 之所以這樣設(shè)計也是出于性能調(diào)優(yōu)的考慮。當(dāng)某線程處理完某批IO數(shù)據(jù)后重新加入等待隊列,由于LIFO機(jī)制,當(dāng)完成隊列中又存在有新的IO數(shù)據(jù)時,該線程將會優(yōu)先處理數(shù)據(jù)。這樣可能會導(dǎo)致某些線程一直處于等待狀態(tài),這樣Windows就可以將其換出內(nèi)存節(jié)約空間。 
  3. 投遞 所謂投遞其實就是利用AcceptEx,WSARecv和WSASend等函數(shù)在IO完成端口中進(jìn)行異步操作。形象來說就是你向洗衣機(jī)輸入?yún)?shù)的過程,后續(xù)工作由洗衣機(jī)(WinSock2)完成。 
 Windows API相關(guān)知識↑
  本節(jié)參考文獻(xiàn) Microsoft. I/O Completion Ports[EB/OL]. https://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx
 Microsoft. Windows Sockets 2[EB/OL]. https://msdn.microsoft.com/en-us/library/windows/desktop/ms740673(v=vs.85).aspx
 Russinovich M E, Solomon D A, Ionescu A. Windows internals[M]. Pearson Education, 2012: 56-58.
 IOCP APIs關(guān)于常規(guī)的IO完成端口API主要有以下三個: 創(chuàng)建和關(guān)聯(lián)IO完成端口函數(shù)CreateIoCompletionPort,該函數(shù)在創(chuàng)建完成端口和關(guān)聯(lián)設(shè)備(文件設(shè)備,套接字等)時使用。 HANDLE WINAPI CreateIoCompletionPort(
    _In_ HANDLE FileHandle,
    _In_opt_ HANDLE ExistingCompletionPort,
    _In_ ULONG_PTR CompletionKey,
    _In_ DWORD NumberOfConcurrentThreads
    );
  獲取完成隊列狀態(tài)函數(shù)
 GetQueuedCompletionStatus,該函數(shù)在線程池線程函數(shù)中使用。? BOOL WINAPI GetQueuedCompletionStatus(
    _In_ HANDLE CompletionPort,
    _Out_ LPDWORD lpNumberOfBytesTransferred,
    _Out_ PULONG_PTR lpCompletionKey,
    _Out_ LPOVERLAPPED * lpOverlapped,
    _In_ DWORD dwMilliseconds
    );
  在完成隊列中插入消息函數(shù)
 PostQueuedCompletionStatus,該函數(shù)在給線程傳遞退出參數(shù)時使用。? BOOL WINAPI PostQueuedCompletionStatus(
    _In_ HANDLE CompletionPort,
    _In_ DWORD dwNumberOfBytesTransferred,
    _In_ ULONG_PTR dwCompletionKey,
    _In_opt_ LPOVERLAPPED lpOverlapped
    );
 
  以上函數(shù)的詳細(xì)用法在參考文獻(xiàn)及piggyXP的文章中可以找到,故不再贅述。 在編程過程中主要考慮以下幾個問題: 
  1. CreateIoCompletionPort函數(shù)的設(shè)計問題 按照設(shè)計模式最基礎(chǔ)的原則即單一職責(zé)原則,這個函數(shù)設(shè)計是存在缺陷的。事實上很多Windows API都或多或少存在此問題,筆者印象比較深刻的是NetBIOS的系列函數(shù)。理想的設(shè)計是自己再抽象兩個函數(shù),即創(chuàng)建完成端口一個函數(shù),綁定完成端口一個函數(shù)??梢赃@樣設(shè)計: 創(chuàng)建一個新的完成端口函數(shù)CreateNewIoCompletionPort,該函數(shù)在初始化時使用。 /**
* Create completion port
*/
inline auto CreateNewIoCompletionPort( DWORD NumberOfConcurrentThreads = 0 ) {
    return CreateIoCompletionPort( INVALID_HANDLE_VALUE, nullptr, 0, NumberOfConcurrentThreads );
}
  設(shè)備與完成端口綁定函數(shù)
 AssociateDeviceWithCompletionPort,該函數(shù)在完成端口建立后與IO設(shè)備綁定時使用。? /**
* Associate device with completion port
*/
inline auto AssociateDeviceWithCompletionPort( HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey ) {
    return CreateIoCompletionPort( hDevice, hCompPort, dwCompKey, 0 ) == hCompPort;
}
 
  2. 線程池線程退出問題 由于在程序中使用了線程池,對于每一個線程而言如何不留痕跡地結(jié)束是一個很有技巧性的問題。一種優(yōu)雅的方法是使用PostQueuedCompletionStatus函數(shù)給完成端口傳遞退出完成鍵(CompletionKey)。由于線程只有可能在等待隊列、已釋放列表和已暫停列表中,且設(shè)計線程函數(shù)時均會循環(huán)調(diào)用GetQueuedCompletionStatus函數(shù),因此最終所有線程都會轉(zhuǎn)移到等待隊列中去。 有的讀者會考慮到等待隊列的LIFO特性,其實只要我們設(shè)計線程函數(shù)時首先判斷傳入的完成鍵是否為退出的特定信號,檢測到自行退出即可。我們在主線程退出時在完成端口中傳入創(chuàng)建線程數(shù)量個推出信號,由于是完成隊列是順序存取,只要線程函數(shù)設(shè)計合理,可以保證每一個線程函數(shù)都可以收到退出消息。不會發(fā)生piggyXP考慮的收不到信息的情況。 
  更深入的討論高級程序員參考 筆者深入分析了
 GetQueuedCompletionStatus函數(shù)(由Kernel32.dll轉(zhuǎn)發(fā),在KernelBase.dll中實現(xiàn)),發(fā)現(xiàn)其內(nèi)部準(zhǔn)備好各項參數(shù)后調(diào)用了NtRemoveIoCompletion函數(shù)(由ntdll.dll轉(zhuǎn)發(fā),在內(nèi)核ntoskrnl.exe中實現(xiàn))。這樣就很明白了,其實就是在完成隊列中取出一個數(shù)據(jù)。 繼續(xù)對NtRemoveIoCompletion函數(shù)進(jìn)行分析,發(fā)現(xiàn)在內(nèi)部調(diào)用了IoRemoveIoCompletion,繼續(xù)深究下去發(fā)現(xiàn)其主要功能調(diào)用了KeRemoveQueueEx函數(shù),而在該函數(shù)內(nèi)部進(jìn)行了無鎖同步: if ( _interlockedbittestandset( ... ) ) {
    do {
        do
            KeYieldProcessorEx( ... );
        while ( ... );
    } while ( _interlockedbittestandset( ... ) );
}
 
  這樣就能保證APC交付時,只有一個線程可以訪問到完成隊列。因此,只要在設(shè)計過程中一次只取出一個完成的數(shù)據(jù),就不會出現(xiàn)問題。當(dāng)然,如果想更高效的處理數(shù)據(jù)(比如調(diào)用GetQueuedCompletionStatusEx)又想通過PostQueuedCompletionStatus方式退出的話,就可能需要特殊處理。比如像piggyXP一樣設(shè)計一個信號量,或者接收到退出信號后在退出之前向完成隊列中再Post一個退出信號等等。 如果想要更加深入的了解其中的運作機(jī)理,大家可以去看看WRK或者是React OS的源碼。當(dāng)然,這些代碼時代都比較久遠(yuǎn)了,可能細(xì)節(jié)上和現(xiàn)在的Windows實現(xiàn)不太一樣,但是也能說明問題。 P.S. 在Windows Vista以上操作系統(tǒng),將完成端口的句柄直接關(guān)閉將取消所有關(guān)聯(lián)的IO操作,關(guān)聯(lián)IO端口的所有線程調(diào)用
 GetQueuedCompletionStatus會放棄等待并立即返回FALSE,這時調(diào)用GetLastError獲取錯誤碼時,會返回ERROR_INVALID_HANDLE。檢測到這一情況就可以退出了。 小插曲 在分析Windows 10內(nèi)核的時候在Explorer中可以看到ntoskrnl,而在IDA中看不到。最后只得將其復(fù)制到其他地方才進(jìn)行了分析,感嘆一句微軟套路深。
 
  3. 完成鍵(CompletionKey)和重疊結(jié)構(gòu)(Overlapped)的設(shè)置問題? 這里可能是理解完成端口的一個難點,至少筆者在學(xué)習(xí)的時候在這里停頓了一段時間。 首先說說完成鍵。這個參數(shù)是為了給線程池中的線程通信而設(shè)計的,也就是說當(dāng)調(diào)用前文所述AssociateDeviceWithCompletionPort時傳入的完成鍵將會傳給調(diào)用GetQueuedCompletionStatus的線程。這樣,主線程就可以通過這兩個函數(shù)與線程池中的線程進(jìn)行通信。同樣注意到完成鍵是一個DWORD類型,也可以給它傳入一個結(jié)構(gòu)體的地址。 而重疊結(jié)構(gòu)是在IO處理時傳遞給相應(yīng)IO函數(shù)的數(shù)據(jù)載體。這個結(jié)構(gòu)很有用,但本文不再展開說明,有興趣的朋友可以查看參考文獻(xiàn)相應(yīng)部分。C/C++程序員應(yīng)該都知道這樣一個事實:結(jié)構(gòu)體的第一個成員的地址和結(jié)構(gòu)體的地址是相同的。所以,我們可以定義一個結(jié)構(gòu)體(或者是一個C++類),將重疊結(jié)構(gòu)作為第一個成員,在IO處理時,將我們定義的結(jié)構(gòu)傳入。這樣,IO函數(shù)處理它自身需要的重疊結(jié)構(gòu)信息,而我們可以在其中夾帶私貨。為什么要這么做呢?因為在我們在線程函數(shù)中可能需要一些其他的數(shù)據(jù),這樣就可以通過這種辦法傳進(jìn)去。 于是我們就明白了:完成鍵與線程有關(guān)而重疊結(jié)構(gòu)與IO有關(guān)。我們需要完成鍵給線程傳遞參數(shù),需要重疊結(jié)構(gòu)(以及夾帶的私貨)來完成IO操作。 至于這些怎樣與Socket結(jié)合,請瀏覽下一節(jié)內(nèi)容。 
  更深入的討論高級程序員參考 在piggyXP的博文中提到了一個“神奇的宏”:
 CONTAINING_RECORD。這個宏廣泛應(yīng)用于驅(qū)動編程中,用于獲取在知道結(jié)構(gòu)體某成員地址的情況下推知整個結(jié)構(gòu)體地址的場景中。具體定義如下: /**
* Calculate the address of the base of the structure given its type, and an
* address of a field within the structure.
*/
#define CONTAINING_RECORD(address, type, field) ((type *)(                                                  (PCHAR)(address) -                                                  (ULONG_PTR)(&((type *)0)->field)))
 
  這個是帶有濃郁C風(fēng)格、充滿trick的一個宏。能進(jìn)行深入討論的朋友一看就明白,就不班門弄斧了。值得注意的是,使用這個宏的時候?qū)Τ蓡T是否是結(jié)構(gòu)體的第一個成員沒有限制。 WinSock2 APIs主要使用的API有如下6個: 創(chuàng)建套接字函數(shù)WSASocket,在創(chuàng)建OVERLAPPED套接字時使用。 
  注意 
 WSASocket是一個宏定義,在MBCS環(huán)境下定義為WSASocketA,在UNICODE環(huán)境下定義為WSASocketW。 SOCKET WSAAPI WSASocketW ( // WSASocketA for MBCS
    _In_ int af,
    _In_ int type,
    _In_ int protocol,
    _In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo,  // LPWSAPROTOCOL_INFOA for MBCS
    _In_ GROUP g,
    _In_ DWORD dwFlags
    );
 綁定函數(shù)bind,在服務(wù)器初始化時使用。 int WSAAPI bind(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
 監(jiān)聽函數(shù)listen,在等待客戶端連接監(jiān)聽時使用。 int WSAAPI listen(
    _In_ SOCKET s,
    _In_ int backlog
    );
 控制套接字函數(shù)WSAIoctl,在獲取函數(shù)指針時使用。 int WSAAPI WSAIoctl(
    _In_ SOCKET s,
    _In_ DWORD dwIoControlCode,
    _In_reads_bytes_opt_(cbInBuffer) LPVOID lpvInBuffer,
    _In_ DWORD cbInBuffer,
    LPVOID lpvOutBuffer,
    _In_ DWORD cbOutBuffer,
    _Out_ LPDWORD lpcbBytesReturned,
    _Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
    _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );
  微軟擴(kuò)展的
 accept函數(shù)AcceptEx,用于接受用戶接入并獲取第一組傳輸?shù)臄?shù)據(jù),代替accept使用。? BOOL PASCAL AcceptEx (
    _In_ SOCKET sListenSocket,
    _In_ SOCKET sAcceptSocket,
    PVOID lpOutputBuffer,
    _In_ DWORD dwReceiveDataLength,
    _In_ DWORD dwLocalAddressLength,
    _In_ DWORD dwRemoteAddressLength,
    _Out_ LPDWORD lpdwBytesReceived,
    _Inout_ LPOVERLAPPED lpOverlapped
    );
 微軟擴(kuò)展的配合解析AcceptEx函數(shù)返回值使用的函數(shù)GetAcceptExSockaddrs,需要獲取第一組數(shù)據(jù)的時候使用。 void GetAcceptExSockaddrs (
    _In_  PVOID      lpOutputBuffer,
    _In_  DWORD      dwReceiveDataLength,
    _In_  DWORD      dwLocalAddressLength,
    _In_  DWORD      dwRemoteAddressLength,
    _Out_ LPSOCKADDR *LocalSockaddr,
    _Out_ LPINT      LocalSockaddrLength,
    _Out_ LPSOCKADDR *RemoteSockaddr,
    _Out_ LPINT      RemoteSockaddrLength
);
  異步接受數(shù)據(jù)函數(shù)
 WSARecv,在接收數(shù)據(jù)時使用。? int WSAAPI WSARecv(
    _In_ SOCKET s,
    _In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers,
    _In_ DWORD dwBufferCount,
    _Out_opt_ LPDWORD lpNumberOfBytesRecvd,
    _Inout_ LPDWORD lpFlags,
    _Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
    _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );
  異步接受數(shù)據(jù)函數(shù)
 WSASend,在接收數(shù)據(jù)時使用。? int WSAAPI WSASend(
    _In_ SOCKET s,
    _In_reads_(dwBufferCount) LPWSABUF lpBuffers,
    _In_ DWORD dwBufferCount,
    _Out_opt_ LPDWORD lpNumberOfBytesSent,
    _In_ DWORD dwFlags,
    _Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
    _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );
 
  以上函數(shù)的詳細(xì)用法在參考文獻(xiàn)及piggyXP的文章中可以找到,故不再贅述。 在編程過程中主要考慮以下幾個問題: 
  1. AcceptEx和GetAcceptExSockaddrs函數(shù)的調(diào)用問題 在實際使用中我們可以發(fā)現(xiàn),調(diào)用這兩個函數(shù)無一不是利用了WSAIoctl返回的函數(shù)指針。筆者在MSDN中也找到了這樣的說法: “The function pointer for the AcceptEx / GetAcceptExSockaddrs  function must be obtained at run time by making a call to the WSAIoctl function with the SIO_GET_EXTENSION_FUNCTION_POINTER opcode specified. “ 因此,我們在使用這兩個函數(shù)之前必須通過WSAIoctl來獲取這兩個函數(shù)的指針加以調(diào)用。 
  更深入的討論高級程序員參考 事實上筆者發(fā)現(xiàn),在
 mswsock.dll中是導(dǎo)出了這兩個函數(shù)的。那為什么微軟在MSDN中沒有說到呢,非要用如此麻煩的方式去調(diào)用AcceptEx和GetAcceptExSockaddrs這兩個函數(shù)? mswsock.dll其實也只是一個轉(zhuǎn)發(fā)器,真實的函數(shù)在另外的地方。在AcceptEx函數(shù)內(nèi)部也會調(diào)用WSAIoctl(在ws2_32.dll中實現(xiàn))來獲取真實的函數(shù)地址。
 有一個非常有意思的地方,AcceptEx函數(shù)除了尋找自己的真實函數(shù)地址以外,還回去尋找GetAcceptExSockaddrs函數(shù)的地址,同時進(jìn)行設(shè)置;在導(dǎo)出的GetAcceptExSockaddrs函數(shù)內(nèi)部不會再去尋找自身實現(xiàn)的地址,而是使用AcceptEx函數(shù)設(shè)置的地址,如果地址為空則將后四個傳入的參數(shù)全部置零,有興趣的朋友可以嘗試一下。 所以使用導(dǎo)出的AcceptEx而不通過指針從理論上也是可以的,在使用導(dǎo)出的GetAcceptExSockaddrs之前務(wù)必要使用導(dǎo)出的AcceptEx來設(shè)置內(nèi)部指針,而且并不是說使用導(dǎo)出的函數(shù)效率低才使用函數(shù)指針獲取函數(shù)實現(xiàn)地址。可能的原因是每個Windows版本的實現(xiàn)驅(qū)動可能不同,對上的接口需要mswsock.dll來保持一致。 另外,在使用這兩個函數(shù)時要注意在傳遞SOCKADDR_IN結(jié)構(gòu)體大小時要加上16,與具體實現(xiàn)相關(guān),原因不明。 
  2. 設(shè)置各函數(shù)完成鍵和重疊結(jié)構(gòu)體的問題 接完成鍵(CompletionKey)和重疊結(jié)構(gòu)(Overlapped)的設(shè)置問題討論。在本文程序中,要設(shè)置完成鍵和重疊結(jié)構(gòu)體的主要有以下6個函數(shù),如表4所示: 表4 需要設(shè)置的函數(shù)及相關(guān)解釋
 大家一看就明白了,AssociateDeviceWithCompletionPort是主線程將創(chuàng)建好的完成端口與IO設(shè)備綁定時調(diào)用的,只需要完成鍵;GetQueuedCompletionStatus函數(shù)是線程池中工作線程調(diào)用的,因此要獲取完成鍵和重疊結(jié)構(gòu);PostQueuedCompletionStatus函數(shù)要傳遞參數(shù)和設(shè)置IO狀態(tài)到完成隊列中去,因此也需要兩個;AcceptEx、WSARecv和WSASend函數(shù)是用來進(jìn)行IO操作(網(wǎng)絡(luò)操作)的,因此只需要和網(wǎng)絡(luò)IO設(shè)備打交道,只需設(shè)置重疊結(jié)構(gòu)。 注意到前述討論中的問題,可以設(shè)計這樣一個結(jié)構(gòu)體充當(dāng)重疊結(jié)構(gòu)夾帶私貨: using IO_CONTEXT = struct _IO_CONTEXT {
        /**
        * data section
        */
        OVERLAPPED  m_olOverLapped;             /**< Windows overlapped structure */
        SOCKET      m_sAssociatedSocket;        /**< context associated socket */
        WSABUF      m_wsaBuffer;                /**< the buffer to recieve WSASocket data */
        CHAR        m_cBuffer[MAX_BUFFER_SIZE]; /**< message buffer */
        enum class Flag : unsigned char {
            Read,                               /**< read( recv ) */
            Write,                              /**< write( send ) */
            Accept                              /**< accept socket( for AcceptEx API ) */
        } m_bFlag;                              /**< rw flag */
        /**
        * operation section
        */
        ...
}
using PIO_CONTEXT = IO_CONTEXT*;
 123456789101112131415161718192021
 注意到完成鍵可以傳入某結(jié)構(gòu)體或類的地址,因此可以設(shè)計這樣一個結(jié)構(gòu)體充當(dāng)完成鍵傳遞給線程池中線程: using HANDLE_CONTEXT = struct _HANDLE_CONTEXT {
        /**
        * data section
        */
        SOCKET      m_hClientSocket;            /**< socket in thread to handle */
        SOCKADDR_IN m_sClientAddr;              /**< sockaddr_in in thread to handle */
        std::vector<PIO_CONTEXT> m_vIoContext;  /**< vector of IoContext pointer */
        bool        m_bFinished;                /**< is process finished */
        /**
        * operation section
        */
        ...
}
using PHANDLE_CONTEXT = HANDLE_CONTEXT*;
 結(jié)構(gòu)定義和piggyXP大同小異,主要差別就在于HANDLE_CONTEXT::m_bFinished項,在PostQueuedCompletionStatus傳遞時將其置為true,讓線程池中線程退出即可。
 上下文大致運行流程如圖2所示,聰明的你一定一下就明白,就不贅述了。可以參考上一節(jié)所述流程,也可以參照代碼理解:
 圖2 各上下文運行大致流程  
 
 幾個問題*↑
  本節(jié)參考文獻(xiàn) ISO. IEC14882:2011 Information technology – Programming languages – C++ [S]. Geneva, Switzerland: International Organization for Standardization, 2011.
 Meyers S. Effective modern C++: 42 specific ways to improve your use of C++ 11 and C++ 14[M]. ” O’Reilly Media, Inc.”, 2014.
 
  提示 這一節(jié)內(nèi)容和本文主體關(guān)系不大,內(nèi)容也不深,對本節(jié)不感興趣的朋友可以跳過。
 function-like macro與inline function的選擇例如piggyXP給出了如下的函數(shù)樣式的宏: // 釋放指針宏
#define RELEASE(x)                      {if(x != NULL ){delete x;x=NULL;}}
 而筆者在定義時選擇了內(nèi)聯(lián)函數(shù): /**
* Release memory
*/
template<typename _T>
inline void ReleaseMemory( _T*& pMemory ) {
    if ( pMemory != nullptr ) {
        delete pMemory;
        pMemory = nullptr;
    }
}
 主要代碼是差不多的,但是能夠完成的操作是不一樣的,聰明的你應(yīng)該可以看出來。這個例子不一定好,那就再舉一個常見的: #define MAX(a, b)       ( (a) > (b) ? (a) : (b) ) 
// oops
int result_oops = MAX(i++, j); 
 選用function-like macro的好處只有一條:簡單方便,效率高(空間換時間),缺點就不多說了,看著就明白。選用inline function最主要的好處就是:類型檢查,效率高(可能空間換時間)。 在編程過程中請盡可能減少預(yù)處理器的使用(尤其是函數(shù)樣式的宏)。 macro constant與compile-time constant(constant expression)我們可能習(xí)慣于這樣定義“常量”: #define MAX_BUFFER_SIZE 8192
 當(dāng)然,這是一個宏,在使用的時候替換為8192這一個字面量??紤]這樣的代碼: #define N 2 + 3
// oops
int oops = N / 2;   // 3
 當(dāng)然你也可以這樣定義,不過總覺得這樣定義很別扭: #define N ( 2 + 3 )
 結(jié)果不用多說。采用宏常量的理由還是:方便、效率高(字面值,在代碼中成為立即數(shù)),但是沒有類型檢查(預(yù)處理器管理),有時候用著很麻煩。 而以往的常量const又占用了存儲空間,而且畢竟存儲在內(nèi)存中,也是可以變化的??紤]以下代碼: const int constant = 0;
int* evil_ptr = ( int* )&constant;
*evil_ptr = 1;
...
 這樣,一個常量就變化了。 
  更深入的討論高級程序員參考 事實上筆者在測試的時候發(fā)現(xiàn)如果對
 constant進(jìn)行輸出,會得到結(jié)果為0。反匯編后發(fā)現(xiàn)VS直接給輸出函數(shù)賦的是0,沒有從地址取值,優(yōu)化的還是可以。 在C++11中引入了常量表達(dá)式constexpr的概念,它是一個編譯期的常量(字面量),由編譯器負(fù)責(zé)執(zhí)行。這樣,又可以進(jìn)行類型檢查,又可以提高效率,減少資源占用,好處還是很多的。其中一個: constexpr std::size_t N = 2 + 3;
// no oops
auto normal = N / 2;    // 2
 |