小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

IOCP服務(wù)器設(shè)計(via Modern C++)

 牛人的尾巴 2018-09-25

鳴謝
感謝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所示:


IOCP狀態(tài)機(jī)
圖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)建的流程就很明了了:?

  1. 創(chuàng)建一個新的完成端口,處理所有的IO請求。
  2. 創(chuàng)建一個線程池,此時線程處于已釋放列表。
  3. 創(chuàng)建一個Socket并將其綁定在創(chuàng)建的完成端口上,作為IO操作的實體。利用這個套接字進(jìn)行Listen操作,并向第1步創(chuàng)建的完成端口中投遞Accept消息,將第2步創(chuàng)建線程置于等待隊列中等待客戶端連接。
  4. 當(dāng)客戶端連接后,IOCP將在IO完成隊列插入Accept等待隊列中的線程將得到Accept,并創(chuàng)建新的Socket作為與客戶端通信的套接字,并將其綁定在第1步創(chuàng)建好的完成端口上。
  5. 此后,無論是Recv,Send都照此步驟進(jìn)行即可。

這里有幾個細(xì)節(jié)需要注意:

1. 最合適的線程數(shù)應(yīng)當(dāng)是多于處理器核心數(shù)的

多線程優(yōu)化理論告誡我們,為了避免ring0ring3之間的上下文切換,我們應(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,WSARecvWSASend等函數(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6


獲取完成隊列狀態(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7


在完成隊列中插入消息函數(shù)PostQueuedCompletionStatus,該函數(shù)在給線程傳遞退出參數(shù)時使用。?

BOOL WINAPI PostQueuedCompletionStatus(
    _In_ HANDLE CompletionPort,
    _In_ DWORD dwNumberOfBytesTransferred,
    _In_ ULONG_PTR dwCompletionKey,
    _In_opt_ LPOVERLAPPED lpOverlapped
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

以上函數(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 );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7


設(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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( ... ) );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這樣就能保證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)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這個是帶有濃郁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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

綁定函數(shù)bind,在服務(wù)器初始化時使用。

int WSAAPI bind(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
  • 1
  • 2
  • 3
  • 4
  • 5

監(jiān)聽函數(shù)listen,在等待客戶端連接監(jiān)聽時使用。

int WSAAPI listen(
    _In_ SOCKET s,
    _In_ int backlog
    );
  • 1
  • 2
  • 3
  • 4

控制套接字函數(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11


微軟擴(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

微軟擴(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
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10


異步接受數(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9


異步接受數(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
    );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以上函數(shù)的詳細(xì)用法在參考文獻(xiàn)及piggyXP的文章中可以找到,故不再贅述。

在編程過程中主要考慮以下幾個問題:

1. AcceptExGetAcceptExSockaddrs函數(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)用AcceptExGetAcceptExSockaddrs這兩個函數(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)解釋

函數(shù) 需要設(shè)置的內(nèi)容 相關(guān)解釋
AssociateDeviceWithCompletionPort 完成鍵 初始化時將完成端口和線程綁定時需要使用
GetQueuedCompletionStatus 完成鍵和重疊結(jié)構(gòu) 線程獲取參數(shù)與IO狀態(tài)時使用
PostQueuedCompletionStatus 完成鍵和重疊結(jié)構(gòu) 傳遞線程參數(shù)與設(shè)置IO狀態(tài)時使用
AcceptEx 重疊結(jié)構(gòu) 在異步接受客戶端接入時使用
WSARecv 重疊結(jié)構(gòu) 在異步接收消息時使用
WSASend 重疊結(jié)構(gòu) 在異步發(fā)送消息時使用

大家一看就明白了,AssociateDeviceWithCompletionPort是主線程將創(chuàng)建好的完成端口與IO設(shè)備綁定時調(diào)用的,只需要完成鍵;GetQueuedCompletionStatus函數(shù)是線程池中工作線程調(diào)用的,因此要獲取完成鍵和重疊結(jié)構(gòu);PostQueuedCompletionStatus函數(shù)要傳遞參數(shù)和設(shè)置IO狀態(tài)到完成隊列中去,因此也需要兩個;AcceptEx、WSARecvWSASend函數(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*;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

注意到完成鍵可以傳入某結(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*;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

結(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;}}
  • 1
  • 2

而筆者在定義時選擇了內(nèi)聯(lián)函數(shù):

/**
* Release memory
*/
template<typename _T>
inline void ReleaseMemory( _T*& pMemory ) {

    if ( pMemory != nullptr ) {

        delete pMemory;
        pMemory = nullptr;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

主要代碼是差不多的,但是能夠完成的操作是不一樣的,聰明的你應(yīng)該可以看出來。這個例子不一定好,那就再舉一個常見的:

#define MAX(a, b)       ( (a) > (b) ? (a) : (b) ) 
// oops
int result_oops = MAX(i++, j); 
  • 1
  • 2
  • 3

選用function-like macro的好處只有一條:簡單方便,效率高(空間換時間),缺點就不多說了,看著就明白。選用inline function最主要的好處就是:類型檢查,效率高(可能空間換時間)。

在編程過程中請盡可能減少預(yù)處理器的使用(尤其是函數(shù)樣式的宏)。

macro constant與compile-time constant(constant expression)

我們可能習(xí)慣于這樣定義“常量”:

#define MAX_BUFFER_SIZE 8192
  • 1

當(dāng)然,這是一個宏,在使用的時候替換為8192這一個字面量??紤]這樣的代碼:

#define N 2 + 3
// oops
int oops = N / 2;   // 3
  • 1
  • 2
  • 3

當(dāng)然你也可以這樣定義,不過總覺得這樣定義很別扭:

#define N ( 2 + 3 )
  • 1

結(jié)果不用多說。采用宏常量的理由還是:方便、效率高(字面值,在代碼中成為立即數(shù)),但是沒有類型檢查(預(yù)處理器管理),有時候用著很麻煩。

而以往的常量const又占用了存儲空間,而且畢竟存儲在內(nèi)存中,也是可以變化的??紤]以下代碼:

const int constant = 0;
int* evil_ptr = ( int* )&constant;
*evil_ptr = 1;
...
  • 1
  • 2
  • 3
  • 4

這樣,一個常量就變化了。

更深入的討論高級程序員參考
事實上筆者在測試的時候發(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
  • 1
  • 2
  • 3

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多