Windows Sockets 2.0:使用完成端口高性能,可擴展性Winsock服務(wù)程序
[摘自]http://blog.csdn.net/vcbear/
翻譯說明:
完成端口基本上公認為一種在
windows服務(wù)平臺上比較成熟和高效的IO方法,理解和編寫程序都不是很困難。目前我正在進行這方面的實踐,代碼還沒有完全調(diào)試和評價,只有這一篇拙劣的學(xué)習(xí)翻譯文摘,見笑見笑。
翻譯這個文章,是因為我近期在學(xué)習(xí)一些
socket服務(wù)程序的編寫中發(fā)現(xiàn)(注意,只是在學(xué)習(xí),我本人在這個領(lǐng)域經(jīng)驗并不充足到可以撰文騙錢的地步:P),如果不是逼著自己把這個文章從頭翻譯一遍,我懷疑我是否能認真領(lǐng)會本文的內(nèi)容 :PPP. 把這個文章貼出來,不是為了賺人氣,而是因為水平確實有限,雖然整體上大差不差的翻譯出來了,但是細節(jié)和用詞上可能還是有很多問題。是希望大家能指出其中的翻譯錯誤和理解謬誤,互相交流和幫助。非常感謝。
本文翻譯并沒有通過原作者同意,僅用來在網(wǎng)絡(luò)上學(xué)習(xí)和交流,加之翻譯水平拙劣,所以請勿用于做商業(yè)用途。
vcbear
2001.8
Windows Sockets 2.0:
使用完成端口高性能,可擴展性Winsock服務(wù)程序
原作者:
Anthony Jones 和Amol Deshpande
原文在http://msdn.microsoft.com/msdnmag/issues/1000/winsock/winsock.asp
- APIs
和擴展性
- 完成端口(
Completion Ports )
- 典型的
Worker Thread 結(jié)構(gòu)
- Windows NT
和 Windows 2000 的Sockets體系結(jié)構(gòu)
- 緩沖區(qū)由誰來管理
- 資源約束
- 關(guān)于接受連接
- TransmitFile
和TransmitPackets函數(shù)
- 來實現(xiàn)一個服務(wù)方案
本文作者假定你已經(jīng)熟悉
Winsock API,TCP/IP ,Win32 API
摘要
:編寫一般的網(wǎng)絡(luò)應(yīng)用程序的難點在于程序的“可擴展性”。利用完成端口進行重疊
I/O的技術(shù)在WindowsNT和WIndows2000上提供了真正的可擴展性。完成端口和Windows Socket2.0結(jié)合可以開發(fā)出支持大量連接的網(wǎng)絡(luò)服務(wù)程序。
本文從討論服務(wù)端的實現(xiàn)開始,然后討論如何處理有系統(tǒng)資源約束和高要求的環(huán)境,以及在可擴展的服務(wù)程序開發(fā)的過程中會遇到的一般問題。
--------------------------------------------------------------------------------
正文:
開發(fā)網(wǎng)絡(luò)程序從來都不是一件容易的事情,盡管只需要遵守很少的一些規(guī)則創(chuàng)建socket,發(fā)起連接,接受連接,發(fā)送和接受數(shù)據(jù)。真正的困難在于:讓你的程序可以適應(yīng)從單單一個連接到幾千個連接。本文主要關(guān)注C/S結(jié)構(gòu)的服務(wù)器端程序,因為一般來說,開發(fā)一個大容量,具可擴展性的winsock程序一般就是指服務(wù)程序。我們將討論基于WindowsNT4.0和Windows 2000的代碼,而不包括Windows3.x(什么時候的東西了),因為Winsock2的這一屬性只在Windows NT4和最新版本上有效。
APIs
和擴展性
win32
重疊I/O(Overlapped I/O)機制允許發(fā)起一個操作,然后在操作完成之后接受到信息。對于那種需要很長時間才能完成的操作來說,重疊IO機制尤其有用,因為發(fā)起重疊操作的線程在重疊請求發(fā)出后就可以自由的做別的事情了。
在
WinNT和Win2000上,提供的真正的可擴展的I/O模型就是使用完成端口(Completion Port)的重疊I/O.
其實類似于
WSAAsyncSelect和select函數(shù)的機制更容易兼容Unix,但是難以實現(xiàn)我們想要的“擴展性”。而且windows的完成端口機制在操作系統(tǒng)內(nèi)部已經(jīng)作了優(yōu)化,提供了更高的效率。所以,我們選擇完成端口開始我們的服務(wù)器程序的開發(fā)。
完成端口(
Completion Ports )
其實可以把完成端口看成系統(tǒng)維護的一個隊列,操作系統(tǒng)把重疊
IO操作完成的事件通知放到該隊列里,由于是暴露 “操作完成”的事件通知,所以命名為“完成端口”(COmpletion Ports)。一個socket被創(chuàng)建后,可以在任何時刻和一個完成端口聯(lián)系起來。
一般來說,一個應(yīng)用程序可以創(chuàng)建多個工作線程來處理完成端口上的通知事件。工作線程的數(shù)量依賴于程序的具體需要。但是在理想的情況下,應(yīng)該對應(yīng)一個
CPU創(chuàng)建一個線程。因為在完成端口理想模型中,每個線程都可以從系統(tǒng)獲得一個“原子”性的時間片,輪番運行并檢查完成端口,線程的切換是額外的開銷。在實際開發(fā)的時候,還要考慮這些線程是否牽涉到其他堵塞操作的情況。如果某線程進行堵塞操作,系統(tǒng)則將其掛起,讓別的線程獲得運行時間。因此,如果有這樣的情況,可以多創(chuàng)建幾個線程來盡量利用時間。
應(yīng)用完成端口分兩步走:
1
創(chuàng)建完成端口句柄:
HANDLE hIocp;
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
注意在第一個參數(shù)(
FileHandle)傳入INVALID_FILE_HANDLE,第二個參數(shù)(ExistingCompletionPort)傳入NULL,系統(tǒng)將創(chuàng)建一個新的完成端口句柄,沒有任何IO句柄與其關(guān)聯(lián)。
2
.
完成端口創(chuàng)建成功后,在socket和完成端口之間建立關(guān)聯(lián)。再次調(diào)用CreateIoCmpletionPort函數(shù),這一次在第一個參數(shù)FileHandle傳入創(chuàng)建的socket句柄,參數(shù)ExistingCompletionPort為已經(jīng)創(chuàng)建的完成端口句柄。
以下代碼創(chuàng)建了一個
socket并把它和完成端口聯(lián)系起來。
SOCKET s;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
// Error
if (CreateIoCompletionPort((HANDLE)s,
hIocp,
(ULONG_PTR)0,
0) == NULL)
{
// Error
}
???
}
到此為止
socket已經(jīng)成功和完成端口相關(guān)聯(lián)。在此socket上進行的重疊IO操作結(jié)果均使用完成端口發(fā)出通知。注意:CreateIoCompletionPort函數(shù)的第三個參數(shù)允許開發(fā)人員傳入一個類型為ULONG_PTR的數(shù)據(jù)成員,我們把它稱為完成鍵(Completion key),此數(shù)據(jù)成員可以設(shè)計為指向包含socket信息的一個結(jié)構(gòu)體的一個指針,用來把相關(guān)的環(huán)境信息和socket聯(lián)系起來,每次完成通知來到的同時,該環(huán)境信息也隨著通知一起返回給開發(fā)人員。
完成端口創(chuàng)建以及與
socket關(guān)聯(lián)之后,就要創(chuàng)建一個或多個工作線程來處理完成通知,每個線程都可以循環(huán)的調(diào)用GetQueuedCompletionStatus函數(shù),檢查完成端口上的通知事件。
在舉例說明一個典型的工作線程的之前,我們先討論一下重疊
IO的過程。當(dāng)一個重疊IO被發(fā)起,一個Overlapped結(jié)構(gòu)體的指針就要作為參數(shù)傳遞給系統(tǒng)。當(dāng)操作完成,GetQueueCompletionStatus可以返回指向同一個Overlapp結(jié)構(gòu)的指針。為了辨認和定位這個已完成的操作,開發(fā)人員最好定義自己的OVERLAPPED結(jié)構(gòu),以包含一些自己定義的關(guān)于操作本身的額外信息。比如:
typedef struct _OVERLAPPEDPLUS {
OVERLAPPED ol;
SOCKET s, sclient;
int OpCode;
WSABUF wbuf;
DWORD dwBytes, dwFlags;
// other useful information
} OVERLAPPEDPLUS;
此結(jié)構(gòu)的第一個成員為默認的
OVERLAPPED結(jié)構(gòu),第二,三個為本地服務(wù)socket和與該操作相關(guān)的客戶socekt,第4個成員為操作類型,對于socket,現(xiàn)在定義的有
#define OP_READ 0
#define OP_WRITE 1
#define OP_ACCEPT 2
3
種。然后還有應(yīng)用程序的socket緩沖區(qū),操作數(shù)據(jù)量,標(biāo)志位以及其他開發(fā)人員認為有用的信息。
當(dāng)進行重疊
IO操作,把OVERLAPPEDPLUS結(jié)構(gòu)作為重疊IO的參數(shù)lpOverlapp傳遞(如WSASend,WASRecv,等函數(shù),有一個lpOverlapped參數(shù),要求傳入一個OVERLAPP結(jié)構(gòu)的指針)
當(dāng)操作完成后,
GetQueuedCompletionStatus函數(shù)返回一個LPOVERLAPPED 類型的指針,這個指針其實是指向開發(fā)人員定義的擴展OVERLAPPEDPLUS結(jié)構(gòu),包含著開發(fā)人員早先傳入的全部信息。
注意
: OVERLAPPED成員不一定要求是OVERLAPPEDPLUS擴展結(jié)構(gòu)的一個成員,在獲得OVERLAPPED指針之后,可以用CONTAINING_RECORD宏獲得相應(yīng)的擴展結(jié)構(gòu)的指針。
典型的
Worker Thread 結(jié)構(gòu)
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPEDPLUS *OverlapPlus,
*newolp;
DWORD dwBytesXfered;
while (1)
{
ret = GetQueuedCompletionStatus(
hIocp,
&dwBytesXfered,
(PULONG_PTR)&PerHandleKey,
&Overlap,
INFINITE);
if (ret == 0)
{
// Operation failed
continue;
}
OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
switch (OverlapPlus->OpCode)
{
case OP_ACCEPT:
// Client socket is contained in OverlapPlus.sclient
// Add client to completion port
CreateIoCompletionPort(
(HANDLE)OverlapPlus->sclient,
hIocp,
(ULONG_PTR)0,
0);
// Need a new OVERLAPPEDPLUS structure
// for the newly accepted socket. Perhaps
// keep a look aside list of free structures.
newolp = AllocateOverlappedPlus();
if (!newolp)
{
// Error
}
newolp->s = OverlapPlus->sclient;
newolp->OpCode = OP_READ;
// This function prepares the data to be sent
PrepareSendBuffer(&newolp->wbuf);
ret = WSASend(
newolp->s,
&newolp->wbuf,
1,
&newolp->dwBytes,
0,
&newolp.ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
// Put structure in look aside list for later use
FreeOverlappedPlus(OverlapPlus);
// Signal accept thread to issue another AcceptEx
SetEvent(hAcceptThread);
break;
case OP_READ:
// Process the data read
// ???
// Repost the read if necessary, reusing the same
// receive buffer as before
memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
ret = WSARecv(
OverlapPlus->s,
&OverlapPlus->wbuf,
1,
&OverlapPlus->dwBytes,
&OverlapPlus->dwFlags,
&OverlapPlus->ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
break;
case OP_WRITE:
// Process the data sent, etc.
break;
} // switch
} // while
} // WorkerThread
--------------------------------------------------------------------------------
查看以上代碼,注意如果
Overlapped操作立刻失敗(比如,返回SOCKET_ERROR或其他非WSA_IO_PENDING的錯誤),則沒有任何完成通知時間會被放到完成端口隊列里。反之,則一定有相應(yīng)的通知時間被放到完成端口隊列。
更完善的關(guān)于
Winsock的完成端口機制,可以參考MSDN的Microsoft PlatFormSDK,那里有完成端口的例子。訪問http://msdn.microsoft.com/library/techart/msdn_servrapp.htm.可以獲得更多信息。
Windows NT
和 Windows 2000 的Sockets體系結(jié)構(gòu)
學(xué)習(xí)一些
WinNT和Win2000基本的Sockets體系結(jié)構(gòu)有益與對擴展性規(guī)則的理解。下圖表示當(dāng)前版本Win2000的Winsock實現(xiàn)。應(yīng)用程序不應(yīng)該依賴于這里描述的一些底層細節(jié)(指drivers ,Dlls之類的),因為這些可能會在未來版本的操作系統(tǒng)中被改變。
Socket 體系結(jié)構(gòu)
Winsock2.0
規(guī)范支持多種協(xié)議以及相關(guān)的支持服務(wù)。這些用戶模式服務(wù)支持可以基于其他現(xiàn)存服務(wù)提供者來擴展他們自己的功能。比如,一個代理層服務(wù)支持(LSP)可以把自己安裝在現(xiàn)存的TCP/IP服務(wù)頂層。這樣,代理服務(wù)就可以截取和重定向一個對底層功能的調(diào)用。
與其他操作系統(tǒng)不同的是,
WinNT和Win2000的傳輸協(xié)議層并不直接給應(yīng)用程序提供socket風(fēng)格的接口,不接受應(yīng)用程序的直接訪問。而是實現(xiàn)了更多的通用API,稱為傳輸驅(qū)動接口(Transport Driver Interface,TDI).這些API把WinNT的子系統(tǒng)從各種各樣的網(wǎng)絡(luò)編程接口中分離出來。然后,通過Winsock內(nèi)核模式驅(qū)動提供了sockets方法(在AFD.SYS里實現(xiàn))。這個驅(qū)動負責(zé)連接和緩沖管理,對應(yīng)用程序提供socket風(fēng)格的編程接口。AFD.SYS則通過TDI和傳輸協(xié)議驅(qū)動層交流數(shù)據(jù)。
緩沖區(qū)由誰來管理
如上所說,對于使用
socket接口和傳輸協(xié)議層交流的應(yīng)用程序來說,AFD.SYS負責(zé)緩沖區(qū)的管理。也就是說,當(dāng)一個程序調(diào)用send或WSASend函數(shù)發(fā)送數(shù)據(jù)的時候,數(shù)據(jù)被復(fù)制到AFD.SYS的內(nèi)部緩沖里(大小根據(jù)SO_SNDBUF設(shè)置),然后send和WSASend立刻返回。之后數(shù)據(jù)由AFD.SYS負責(zé)發(fā)送到網(wǎng)絡(luò)上,與應(yīng)用程序無關(guān)。當(dāng)然,如果應(yīng)用程序希望發(fā)送比SO_SNDBUF設(shè)置的緩沖區(qū)還大的數(shù)據(jù),WSASend函數(shù)將會被堵塞,直到所有數(shù)據(jù)均被發(fā)送完畢為止。
同樣,當(dāng)從遠地客戶端接受數(shù)據(jù)的時候,如果應(yīng)用程序沒有提交
receive請求,而且線上數(shù)據(jù)沒有超出SO_RCVBUF設(shè)置的緩沖大小,那么AFD.SYS就把網(wǎng)絡(luò)上的數(shù)據(jù)復(fù)制到自己的內(nèi)部緩沖保存。當(dāng)應(yīng)用程序調(diào)用recv或WSARecv函數(shù)的時候,數(shù)據(jù)即從AFD.SYS的緩沖復(fù)制到應(yīng)用程序提供的緩沖區(qū)里。
在大多數(shù)情況下,這個體系工作的很好。尤其是應(yīng)用程序使用一般的發(fā)送接受例程不牽涉使用
Overlapped的時候。開發(fā)人員可以通過使用setsockopt API函數(shù)把SO_SNDBUF和SO_RCVBUF這兩個設(shè)置的值改為0關(guān)閉AFD.SYS的內(nèi)部緩沖。但是,這樣做會帶來一些后果:
比如,應(yīng)用程序把
SO_SNDBUF設(shè)為0,關(guān)閉了發(fā)送緩沖(指AFD.SYS里的緩沖),并發(fā)出一個同步堵塞式的發(fā)送操作,應(yīng)用程序提供的數(shù)據(jù)緩沖區(qū)就會被內(nèi)核鎖定,send函數(shù)不會返回,直到連接的另一端收到整個緩沖區(qū)的數(shù)據(jù)為止。這貌似一種挺不錯的方法,用來判斷是否你的數(shù)據(jù)已經(jīng)被對方全部收取。但實際上,這是很糟糕的。問題在于:網(wǎng)絡(luò)層即使收到遠端TCP的確認,也不能保證數(shù)據(jù)會被安全交到客戶端應(yīng)用程序那里,因為客戶端可能發(fā)生“資源不足”等情況,而導(dǎo)致應(yīng)用程序無法從AFD.SYS的內(nèi)部緩沖復(fù)制得到數(shù)據(jù)。而更重大的問題是:由于堵塞,程序在一個線程里只能進行一次send操作,非常的沒有效率。
如果關(guān)閉接受緩沖(設(shè)置
SO_RCVBUF的值為0),也不能真正的提高效率。接受緩沖為0迫使接受的數(shù)據(jù)在比winsock內(nèi)核層更底層的地方被緩沖,同樣在調(diào)用recv的時候進行才進行緩沖復(fù)制,這樣你關(guān)閉AFD緩沖的根本意圖(避免緩沖復(fù)制)就落空了。關(guān)閉接收緩沖是沒有必要的,只要應(yīng)用程序經(jīng)常有意識的在一個連接上調(diào)用重疊WSARecvs操作,這樣就避免了AFD老是要緩沖大量的到來數(shù)據(jù)。
到這里,我們應(yīng)該清楚關(guān)閉緩沖的方法對絕大多數(shù)應(yīng)用程序來說沒有太多好處的了。
然而,一個高性能的服務(wù)程序可以關(guān)閉發(fā)送緩沖,而不影響性能。這樣的程序必須確保它在同時執(zhí)行多個
Overlapped發(fā)送,而不是等待一個Overlapped發(fā)送結(jié)束之后,才執(zhí)行另一個。這樣如果一個數(shù)據(jù)緩沖區(qū)數(shù)據(jù)已經(jīng)被提交,那么傳輸層就可以立刻使用該數(shù)據(jù)緩沖區(qū)。如果程序“串行”的執(zhí)行Overlapped發(fā)送,就會浪費一個發(fā)送提交之后另一個發(fā)送執(zhí)行之前那段時間。
資源約束
魯棒性是每一個服務(wù)程序的一個主要設(shè)計目標(biāo)。就是說,服務(wù)程序應(yīng)該可以對付任何的突發(fā)問題,比如,客戶端請求的高峰,可用內(nèi)存的暫時貧缺,以及其他可靠性問題。為了平和的解決這些問題,開發(fā)人員必須了解典型的
WindowsNT和Windows2000平臺上的資源約束。
最基本的問題是網(wǎng)絡(luò)帶寬。使用
UDP協(xié)議進行發(fā)送的服務(wù)程序?qū)Υ艘筝^高,因為這樣的服務(wù)程序要求盡量少的丟包率。即使是使用TCP連接,服務(wù)器也必須注意不要濫用網(wǎng)絡(luò)資源。否則,TCP連接中將會出現(xiàn)大量重發(fā)和連接取消事件。具體的帶寬控制是跟具體程序相關(guān)的,超出了本文的討論范圍。
程序所使用的虛擬內(nèi)存也必須小心。應(yīng)該保守的執(zhí)行內(nèi)存申請和釋放,或許可以使用旁視列表(一個記錄申請并使用過的“空閑”內(nèi)存的緩沖區(qū))來重用已經(jīng)申請但是被程序使用過,空閑了的內(nèi)存,這樣可以使服務(wù)程序避免過多的反復(fù)申請內(nèi)存,并且保證系統(tǒng)中一直有盡可能多的空余內(nèi)存。(應(yīng)用程序還可以使用
SetWorkingSetSize這個Win32API函數(shù)來向系統(tǒng)請求增加該程序可用的物理內(nèi)存。)
有兩個
winsock程序不會直接面對的資源約束。第一個是頁面鎖定限制。無論應(yīng)用程序發(fā)起send還是receive操作,也不管AFD.SYS的緩沖是否被禁止,數(shù)據(jù)所在的緩沖都會被鎖定在物理內(nèi)存里。因為內(nèi)核驅(qū)動要訪問該內(nèi)存的數(shù)據(jù),在訪問期間該內(nèi)存區(qū)域都不能被解鎖。在大部分情況下,這不會產(chǎn)生任何問題。但是操作系統(tǒng)必須確認還有可用的可分頁內(nèi)存來提供給其他程序。這樣做的目的是防止一個有錯誤操作的程序請求鎖定所有的物理RAM,而導(dǎo)致系統(tǒng)崩潰。這意味著,應(yīng)用程序必須有意識的避免導(dǎo)致過多頁面鎖定,使該數(shù)量達到或超過系統(tǒng)限制。
在
WinNT和Win2000中,系統(tǒng)允許的總共的內(nèi)存鎖定的限制大概是物理內(nèi)存的1/8。這只是粗略的估計,不能作為一個準(zhǔn)確的計算數(shù)據(jù)。只是需要知道,有時重疊IO操作會發(fā)生ERROR_INSUFFICIENT_RESOURCE失敗,這是因為可能同時有太多的send/receives操作在進行中。程序應(yīng)該注意避免這種情況。
另一個的資源限制情況是,程序運行時,系統(tǒng)達到非分頁內(nèi)存池的限制。
WinNT和Win2000的驅(qū)動從指定的非分頁內(nèi)存池中申請內(nèi)存。這個區(qū)域里分配的內(nèi)存不會被扇出,因為它包含了多個不同的內(nèi)核對象可能需要訪問的數(shù)據(jù),而有些內(nèi)核對象是不能訪問已經(jīng)扇出的內(nèi)存的。一旦系統(tǒng)創(chuàng)建了一個socket (或打開一個文件),一定數(shù)目的非分頁內(nèi)存就被分配了。另外,綁定(binding)和連接socket也會導(dǎo)致額外的非分頁內(nèi)存池的分配。更進一步的說,一個I/O請求,比如send或receive,也是分配了很少的一點非分頁內(nèi)存池的(為了跟蹤I/O操作的進行,包含必須信息的一個很小的結(jié)構(gòu)體被分配了)。積少成多,最后還是可能導(dǎo)致問題。因此操作系統(tǒng)限制了非分頁內(nèi)存的數(shù)量。在winNT和win2000平臺上,每個連接分配的非分頁內(nèi)存的準(zhǔn)確數(shù)量是不相同的,在未來的windows版本上也可能保持差異。如果你想延長你的程序的壽命,就不要打算在你的程序中精確的計算和控制你的非分頁內(nèi)存的數(shù)量。
雖然不能準(zhǔn)確計算,但是程序在策略上要注意避免沖擊非分頁限制。當(dāng)系統(tǒng)的非分頁池內(nèi)存枯竭,一個跟你的程序完全無關(guān)的的驅(qū)動都有可能出問題,因為它無法正常的申請到非分頁內(nèi)存。最壞的情況下,會導(dǎo)致整個系統(tǒng)崩潰。比如那些第三方設(shè)備或系統(tǒng)本身的驅(qū)動。切記:在同一臺計算機上,可能還有其他的服務(wù)程序在運行,同樣在消耗非分頁內(nèi)存。開發(fā)人員應(yīng)該用最保守的策略估算資源,并基于此策略開發(fā)程序。
資源約束的解決方案是很復(fù)雜的,因為事實上,當(dāng)資源不足的情況發(fā)生時,可能不會有特定的錯誤代碼返回到程序。程序在調(diào)用函數(shù)時可能可以得到類似
WSAENOBUFS或
ERROR_INSUFFICIENT_RESOURCES
的這種一般的返回代碼。如何處理這些錯誤呢,首先,合理的增加程序的工作環(huán)境設(shè)置(Working set,如果想獲得更多信息,請參考MSDN里John Robbins關(guān)于 Bugslayer的一章)。如果仍然不能解決問題,那么你可能遇上了非分頁內(nèi)存池限制。那么最好是立刻關(guān)閉部分連接,并期待情況恢復(fù)正常。
關(guān)于接受連接
服務(wù)程序最常做的一個事情是接受客戶端的連接。
AcceptEx函數(shù)是Winsock API中唯一可以使用重疊IO方式接受Socket連接的函數(shù)。AccpetEx要求一個傳入一個socket 作為它的參數(shù)。普通的同步accept函數(shù),新的SOCKET是作為返回值得到的。AcceptEx函數(shù)作為一個重疊操作,接收Socket應(yīng)該提前被創(chuàng)建(但不需要綁定和或連接),并傳入此API。
(
AcceptEx原形,加粗的即為需要傳入的socket
BOOL AcceptEx(
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
);
)
使用
AcceptEx的例程可能是這個樣子的:
do {
-Wait for a previous AcceptEx to complete //
等待前一個AcceptEx完成
-Create a new socket and associate it with the completion port //
創(chuàng)建一個新的Socket并將其關(guān)聯(lián)
//
到完成端口
-Allocate context structure etc. //
初始化相關(guān)的環(huán)境信息結(jié)構(gòu)
-Post an AcceptEx request. //
進入AcceptEx請求。
}
while(TRUE);
一個服務(wù)器一直具備足夠的
AcceptEx調(diào)用,這樣就可以立刻響應(yīng)客戶機的連接。AcceptEx操作的數(shù)量取決于服務(wù)器的策略。如果要滿足高連接率(比如大量的短暫連接或爆發(fā)性的流量)的話,當(dāng)然比不常發(fā)生連接的程序需要更多的AcceptEx入口。聰明的策略就是根據(jù)流量改變AcceptEx調(diào)用的數(shù)量,而避免只使用一個確定的數(shù)目。
在
Win2000上,Winsock提供了一些幫助,用來判斷AcceptEx調(diào)用的數(shù)量是否跟不上需要。當(dāng)創(chuàng)建一個監(jiān)聽Socket之后,使用WSAEventSelect函數(shù)把它和一個FD_ACCEPT事件關(guān)聯(lián),如果沒有accept未決的調(diào)用正在進行,一旦有請求到來,該事件(FD_ACCEPT)就會發(fā)生。因此此事件可以用來告訴開發(fā)人員:還需要進行更多的AcceptEx操作,或者由此探測到一個有異常行為的遠端實體。注意:此機制在NT上是無效的。
使用
AcceptEx的顯著好處是:在一次連接中就可以獲取客戶端的數(shù)據(jù),見AcceptEx的lpOutputBuffer參數(shù)。這意味著如果客戶端連接并立刻發(fā)送數(shù)據(jù)的話,AcceptEx將在客戶端連接成功和數(shù)據(jù)發(fā)送之后才完成。這個功能同時導(dǎo)致的問題是:AcceptEx必須等待數(shù)據(jù)接受完成才能返回。因為一個帶
Output緩沖的AcceptEx函數(shù)并非一個“原子”操作,而是兩步的過程:接受連接和等待數(shù)據(jù)。所以程序在數(shù)據(jù)接受之前并不會知道連接成功。當(dāng)然客戶端也可以連接到服務(wù)器而不馬上發(fā)送數(shù)據(jù),如果這樣的連接過多,服務(wù)器將開始拒絕合法的連接,因為沒有可用的未決的Accept操作入口。這也是一種常用的方法,通過拒絕訪問,防止惡意攻擊和海量連接。
在正在接受連接的線程中,可以檢查
AcceptEx調(diào)用傳入的socket,調(diào)用getsockopt檢查其SO_CONNECT_TIME,該值返回的是socket連接的時間,沒有連接的時候返回-1。
根據(jù)
WSAEventSelect機制所帶來的特性,我們可以很容易的判斷是否應(yīng)該檢查傳到AcceptEx函數(shù)的socket句柄的連接時間。如果在一定時間里,AcceptEx沒有從某個連接中收到數(shù)據(jù),AcceptEx可以通過關(guān)閉該socket來斷開連接。在不緊急的情況下,程序不應(yīng)該關(guān)閉一個AcceptEx里處于未連接狀態(tài)的socket ,因為系統(tǒng)考慮到性能問題,關(guān)聯(lián)在AcceptEx上的內(nèi)核態(tài)數(shù)據(jù)結(jié)構(gòu)不會被釋放,直到一個新的連接到來或監(jiān)聽socket本身都關(guān)閉了。
乍看起來,一個發(fā)出
AcceptEx請求的線程同時也可以是一個關(guān)聯(lián)在完成端口上,并且處理其他完成IO事件的工作線程。然而,最好不要設(shè)計這樣一個線程。在winsocket2的層次結(jié)構(gòu)上有一個副作用,那就是一個socket/WSASocket API的開銷是相當(dāng)可觀的,每個AccepEx都需要創(chuàng)建一個新的socket,所以最好創(chuàng)建一個單獨的跟其他IO處理無關(guān)的線程來調(diào)用AcceptEx。當(dāng)然,你還可以利用這個線程來進行其他的工作如創(chuàng)建事件log
關(guān)于
AcceptEx要注意的最后一個事情是:Winsock2的其他供應(yīng)商不一定會實現(xiàn)AcceptEx函數(shù)。同樣情況也包括的其他Microsoft的特定APIs如TransmitFile,GetAcceptExSockAddrs以及其他Microsoft將在以后版本的windows里。在運行WinNT和Win2000的系統(tǒng)上,這些APIs在Microsoft提供的DLL(mswsock.dll)里實現(xiàn),可以通過鏈接mswsock.lib或者通過WSAioctl的SIO_GET_EXTENSION_FUNCTION_POINTER操作動態(tài)調(diào)用這些擴展APIs.
未獲取函數(shù)指針就調(diào)用函數(shù)(如直接連接
mswsock..lib并直接調(diào)用AcceptEx)的消耗是很大的,因為AcceptEx 實際上是存在于Winsock2結(jié)構(gòu)體系之外的。每次應(yīng)用程序常試在服務(wù)提供層上(mswsock之上)調(diào)用AcceptEx時,都要先通過WSAIoctl獲取該函數(shù)指針。如果要避免這個很影響性能的操作,應(yīng)用程序最好是直接從服務(wù)提供層通過WSAIoctl先獲取這些APIs的指針。
TransmitFile
和TransmitPackets函數(shù)
Winsock
提供了兩個專為文件和內(nèi)存數(shù)據(jù)傳輸而優(yōu)化過的函數(shù)。TransmitFile API在WinNT和Win2000均有效,而TransmitPackets作為一個新的擴展函數(shù),將在未來版本的windows里實現(xiàn)。
TransmitFile
可以把文件的內(nèi)容通過socket傳輸。一般情況下,如果應(yīng)用程序通過socket傳輸文件,首先要用CreateFile打開文件,并循環(huán)調(diào)用ReadFile和WSASend函數(shù),讀取一段數(shù)據(jù)然后發(fā)送,直到整個文件發(fā)送完畢。這樣的效率很低,因為ReadFile和WSASend調(diào)用都需要系統(tǒng)在用戶態(tài)和核心態(tài)之間進行轉(zhuǎn)換。TransmitFile則只需要知道需要傳輸?shù)奈募浔鸵獋鬏數(shù)淖止?jié)數(shù),只有CreateFile打開文件獲得句柄這個向核心態(tài)躍遷的這一個額外開銷。如果你的程序需要通過socket發(fā)送大量文件,建議使用此函數(shù)。
函數(shù)的原形如下:
BOOL TransmitFile(
SOCKET hSocket,
HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwFlags
);
TransmitPackets API
比TransmitFile API更進一步,允許調(diào)用者一次指定多個文件句柄和內(nèi)存緩沖區(qū),并進行傳輸。原形如下:
BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKET_ELEMENT lpPacketArray,
DWORD nElementCount,
DWORD nSendSize,
LPOVERLAPPED lpOverlapped,
DWORD dwFlags
);
lpPacketArray
包含結(jié)構(gòu)體的數(shù)組。每個入口點指定一個需要被傳輸?shù)奈募浔騼?nèi)存緩沖,該結(jié)構(gòu)的成員如下:
typedef struct _TRANSMIT_PACKETS_ELEMENT {
DWORD dwElFlags;
DWORD cLength;
union {
struct {
LARGE_INTEGER nFileOffset;
HANDLE hFile;
};
PVOID pBuffer;
};
} TRANSMIT_FILE_BUFFERS;
各成員的名字都是自解釋的。
dwEIFlags成員指明結(jié)構(gòu)體里的元素是一個文件句柄(TF_ELEMENT_FILE)還是一個內(nèi)存緩沖(TF_ELEMENT_MEMORY)。cLength成員表示要傳輸?shù)淖止?jié)數(shù)(對于文件句柄,0則表示文件內(nèi)所有的數(shù)據(jù))。一個未命名的聯(lián)合體(union)包含內(nèi)存緩沖指針或文件句柄(以及指定的偏移量)。
使用這兩個函數(shù)的其他好處是,你可以通過指定
TF_REUSE_SOCKET標(biāo)志(必須同時指定TF_DISCONNECT標(biāo)志)重用socket句柄。一旦API函數(shù)完成數(shù)據(jù)傳輸,連接就會在傳輸點的層次上斷開,然后該socket就可以被AcceptEx重新使用。這樣就可以減少反復(fù)創(chuàng)建socket和的次數(shù),優(yōu)化了效率。
使用這兩個函數(shù)要注意的是,在
WinNT Workstation版本或win2000 Professional版本上,并不能實現(xiàn)優(yōu)化性能。必須在winNT,win2000 Server ,Win2000 Advanced Server 或Win2000 Data Center版本上才能實現(xiàn)。
來實現(xiàn)一個服務(wù)方案
在前幾章里,我們介紹了一些有益于改善性能提高擴展性的
APIs和方法,以及可能遇到的資源瓶頸。這對你有用嗎?當(dāng)然,首先取決于你的服務(wù)端和客戶端的設(shè)計。在設(shè)計時,你對服務(wù)端和客戶端的控制越得力,你就能更好的避免瓶頸
讓我們來看一種簡單的用例,在這個用例里我們設(shè)計一個服務(wù)器,這個服務(wù)器處理客戶端的連接,然后客戶端發(fā)送一次數(shù)據(jù),并期待服務(wù)端的回應(yīng),然后客戶端斷開連接。
我們的設(shè)計是:服務(wù)器創(chuàng)建一個監(jiān)聽
socket,并和一個完成端口相關(guān)聯(lián),然后創(chuàng)建和CPU同等數(shù)量的工作線程,以及一個專門用來進行AcceptEx調(diào)用的線程。既然我們知道客戶端一旦連接馬上就會發(fā)送數(shù)據(jù) ,那么準(zhǔn)備一個接受緩沖區(qū)會有利于工作的進行。當(dāng)然,不要忘記經(jīng)常的檢查正在連接的socket的SO_CONNECT_TIME值,避免死連接。
本設(shè)計中重要的一項是決定需要顯形調(diào)用多少個
AcceptEx。因為每個AcceptEx操作都需要一個接收緩沖區(qū),大量的頁面將被鎖定(還記得每個重疊操作都會消耗一些非分頁內(nèi)存,并且會將一些數(shù)據(jù)緩沖鎖定到內(nèi)存里嗎)。沒有公式和具體的準(zhǔn)則指導(dǎo)如何確定究竟允許多少個AcceptEx操作。最好的方案就是使這個數(shù)目成為可調(diào)的,通過性能測試,尋找一個在典型的環(huán)境下最好的值
現(xiàn)在已經(jīng)確定服務(wù)器是如何處理連接的了,下一步就是發(fā)送數(shù)據(jù)。影響發(fā)送數(shù)據(jù)的重要因素就是你期望服務(wù)器能夠并發(fā)的處理連接數(shù)。一般來說,服務(wù)器應(yīng)該限制并發(fā)的連接數(shù)量,以及顯式的
send調(diào)用。越多的連接數(shù)意味著越多的非分頁內(nèi)存的使用,并發(fā)的send調(diào)用也應(yīng)該被限制,避免沖擊系統(tǒng)的可分頁內(nèi)存鎖定極限。連接數(shù)和并發(fā)的send調(diào)用限制也都應(yīng)該是程序可調(diào)節(jié)的。
在本例的情況里,不必要去取消每個
socket的接收緩沖,因為接收事件僅僅在AcceptEx調(diào)用中發(fā)生。保證每個socket都有一個接收緩沖不會造成什么危害。一旦客戶端/服務(wù)器在最初的一次請求(由AcceptEx完成)之后進行交互,發(fā)送更多的數(shù)據(jù),那么取消接收緩沖更是一個很不好的做法。除非你能保證這些數(shù)據(jù)都是在每個連接的重疊IO接收里完成的 。
結(jié)束語:
重復(fù):開發(fā)一個可擴展的
Winsock服務(wù)器并非十分困難的。僅僅是開始一個監(jiān)聽socket,接收連接,并且進行重疊發(fā)送和接收的IO操作。最大的挑戰(zhàn)就是管理系統(tǒng)資源,限制重疊Io的數(shù)量,避免內(nèi)存危機。遵循這幾個原則,就能幫助你開發(fā)高性能,可擴展的服務(wù)程序。