翻譯說明:
完成端口基本上公認(rèn)為一種在
windows服務(wù)平臺上比較成熟和高效的IO方法,理解和編寫程序都不是很困難。目前我正在進(jìn)行這方面的實(shí)踐,代碼還沒有完全調(diào)試和評價(jià),只有這一篇拙劣的學(xué)習(xí)翻譯文摘,見笑見笑。
翻譯這個(gè)文章,是因?yàn)槲医谠趯W(xué)習(xí)一些
socket服務(wù)程序的編寫中發(fā)現(xiàn)(注意,只是在學(xué)習(xí),我本人在這個(gè)領(lǐng)域經(jīng)驗(yàn)并不充足到可以撰文騙錢的地步:P),如果不是逼著自己把這個(gè)文章從頭翻譯一遍,我懷疑我是否能認(rèn)真領(lǐng)會本文的內(nèi)容 :PPP. 把這個(gè)文章貼出來,不是為了賺人氣,而是因?yàn)樗酱_實(shí)有限,雖然整體上大差不差的翻譯出來了,但是細(xì)節(jié)和用詞上可能還是有很多問題。是希望大家能指出其中的翻譯錯誤和理解謬誤,互相交流和幫助。非常感謝。
本文翻譯并沒有通過原作者同意,僅用來在網(wǎng)絡(luò)上學(xué)習(xí)和交流,加之翻譯水平拙劣,所以請勿用于做商業(yè)用途。
vcbear
2001.8
Windows Sockets 2.0:
使用完成端口高性能,可擴(kuò)展性Winsock服務(wù)程序
原作者:
Anthony Jones 和Amol Deshpande
原文在http://msdn.microsoft.com/msdnmag/issues/1000/winsock/winsock.asp
- APIs
和擴(kuò)展性
- 完成端口(
Completion Ports )
- 典型的
Worker Thread 結(jié)構(gòu)
- Windows NT
和 Windows 2000 的Sockets體系結(jié)構(gòu)
- 緩沖區(qū)由誰來管理
- 資源約束
- 關(guān)于接受連接
- TransmitFile
和TransmitPackets函數(shù)
- 來實(shí)現(xiàn)一個(gè)服務(wù)方案
本文作者假定你已經(jīng)熟悉
Winsock API,TCP/IP ,Win32 API
摘要
:編寫一般的網(wǎng)絡(luò)應(yīng)用程序的難點(diǎn)在于程序的“可擴(kuò)展性”。利用完成端口進(jìn)行重疊
I/O的技術(shù)在WindowsNT和WIndows2000上提供了真正的可擴(kuò)展性。完成端口和Windows Socket2.0結(jié)合可以開發(fā)出支持大量連接的網(wǎng)絡(luò)服務(wù)程序。
本文從討論服務(wù)端的實(shí)現(xiàn)開始,然后討論如何處理有系統(tǒng)資源約束和高要求的環(huán)境,以及在可擴(kuò)展的服務(wù)程序開發(fā)的過程中會遇到的一般問題。
--------------------------------------------------------------------------------
正文:
開發(fā)網(wǎng)絡(luò)程序從來都不是一件容易的事情,盡管只需要遵守很少的一些規(guī)則創(chuàng)建socket,發(fā)起連接,接受連接,發(fā)送和接受數(shù)據(jù)。真正的困難在于:讓你的程序可以適應(yīng)從單單一個(gè)連接到幾千個(gè)連接。本文主要關(guān)注C/S結(jié)構(gòu)的服務(wù)器端程序,因?yàn)橐话銇碚f,開發(fā)一個(gè)大容量,具可擴(kuò)展性的winsock程序一般就是指服務(wù)程序。我們將討論基于WindowsNT4.0和Windows 2000的代碼,而不包括Windows3.x(什么時(shí)候的東西了),因?yàn)?/font>Winsock2的這一屬性只在Windows NT4和最新版本上有效。
APIs
和擴(kuò)展性
win32
重疊I/O(Overlapped I/O)機(jī)制允許發(fā)起一個(gè)操作,然后在操作完成之后接受到信息。對于那種需要很長時(shí)間才能完成的操作來說,重疊IO機(jī)制尤其有用,因?yàn)榘l(fā)起重疊操作的線程在重疊請求發(fā)出后就可以自由的做別的事情了。
在
WinNT和Win2000上,提供的真正的可擴(kuò)展的I/O模型就是使用完成端口(Completion Port)的重疊I/O.
其實(shí)類似于
WSAAsyncSelect和select函數(shù)的機(jī)制更容易兼容Unix,但是難以實(shí)現(xiàn)我們想要的“擴(kuò)展性”。而且windows的完成端口機(jī)制在操作系統(tǒng)內(nèi)部已經(jīng)作了優(yōu)化,提供了更高的效率。所以,我們選擇完成端口開始我們的服務(wù)器程序的開發(fā)。
完成端口(
Completion Ports )
其實(shí)可以把完成端口看成系統(tǒng)維護(hù)的一個(gè)隊(duì)列,操作系統(tǒng)把重疊
IO操作完成的事件通知放到該隊(duì)列里,由于是暴露 “操作完成”的事件通知,所以命名為“完成端口”(COmpletion Ports)。一個(gè)socket被創(chuàng)建后,可以在任何時(shí)刻和一個(gè)完成端口聯(lián)系起來。
一般來說,一個(gè)應(yīng)用程序可以創(chuàng)建多個(gè)工作線程來處理完成端口上的通知事件。工作線程的數(shù)量依賴于程序的具體需要。但是在理想的情況下,應(yīng)該對應(yīng)一個(gè)
CPU創(chuàng)建一個(gè)線程。因?yàn)樵谕瓿啥丝诶硐肽P椭校總€(gè)線程都可以從系統(tǒng)獲得一個(gè)“原子”性的時(shí)間片,輪番運(yùn)行并檢查完成端口,線程的切換是額外的開銷。在實(shí)際開發(fā)的時(shí)候,還要考慮這些線程是否牽涉到其他堵塞操作的情況。如果某線程進(jìn)行堵塞操作,系統(tǒng)則將其掛起,讓別的線程獲得運(yùn)行時(shí)間。因此,如果有這樣的情況,可以多創(chuàng)建幾個(gè)線程來盡量利用時(shí)間。
應(yīng)用完成端口分兩步走:
1
創(chuàng)建完成端口句柄:
HANDLE hIocp;
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
注意在第一個(gè)參數(shù)(
FileHandle)傳入INVALID_FILE_HANDLE,第二個(gè)參數(shù)(ExistingCompletionPort)傳入NULL,系統(tǒng)將創(chuàng)建一個(gè)新的完成端口句柄,沒有任何IO句柄與其關(guān)聯(lián)。
2
.
完成端口創(chuàng)建成功后,在socket和完成端口之間建立關(guān)聯(lián)。再次調(diào)用CreateIoCmpletionPort函數(shù),這一次在第一個(gè)參數(shù)FileHandle傳入創(chuàng)建的socket句柄,參數(shù)ExistingCompletionPort為已經(jīng)創(chuàng)建的完成端口句柄。
以下代碼創(chuàng)建了一個(gè)
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上進(jìn)行的重疊IO操作結(jié)果均使用完成端口發(fā)出通知。注意:CreateIoCompletionPort函數(shù)的第三個(gè)參數(shù)允許開發(fā)人員傳入一個(gè)類型為ULONG_PTR的數(shù)據(jù)成員,我們把它稱為完成鍵(Completion key),此數(shù)據(jù)成員可以設(shè)計(jì)為指向包含socket信息的一個(gè)結(jié)構(gòu)體的一個(gè)指針,用來把相關(guān)的環(huán)境信息和socket聯(lián)系起來,每次完成通知來到的同時(shí),該環(huán)境信息也隨著通知一起返回給開發(fā)人員。
完成端口創(chuàng)建以及與
socket關(guān)聯(lián)之后,就要創(chuàng)建一個(gè)或多個(gè)工作線程來處理完成通知,每個(gè)線程都可以循環(huán)的調(diào)用GetQueuedCompletionStatus函數(shù),檢查完成端口上的通知事件。
在舉例說明一個(gè)典型的工作線程的之前,我們先討論一下重疊
IO的過程。當(dāng)一個(gè)重疊IO被發(fā)起,一個(gè)Overlapped結(jié)構(gòu)體的指針就要作為參數(shù)傳遞給系統(tǒng)。當(dāng)操作完成,GetQueueCompletionStatus可以返回指向同一個(gè)Overlapp結(jié)構(gòu)的指針。為了辨認(rèn)和定位這個(gè)已完成的操作,開發(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)的第一個(gè)成員為默認(rèn)的
OVERLAPPED結(jié)構(gòu),第二,三個(gè)為本地服務(wù)socket和與該操作相關(guān)的客戶socekt,第4個(gè)成員為操作類型,對于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ā)人員認(rèn)為有用的信息。
當(dāng)進(jìn)行重疊
IO操作,把OVERLAPPEDPLUS結(jié)構(gòu)作為重疊IO的參數(shù)lpOverlapp傳遞(如WSASend,WASRecv,等函數(shù),有一個(gè)lpOverlapped參數(shù),要求傳入一個(gè)OVERLAPP結(jié)構(gòu)的指針)
當(dāng)操作完成后,
GetQueuedCompletionStatus函數(shù)返回一個(gè)LPOVERLAPPED 類型的指針,這個(gè)指針其實(shí)是指向開發(fā)人員定義的擴(kuò)展OVERLAPPEDPLUS結(jié)構(gòu),包含著開發(fā)人員早先傳入的全部信息。
注意
: OVERLAPPED成員不一定要求是OVERLAPPEDPLUS擴(kuò)展結(jié)構(gòu)的一個(gè)成員,在獲得OVERLAPPED指針之后,可以用CONTAINING_RECORD宏獲得相應(yīng)的擴(kuò)展結(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的錯誤),則沒有任何完成通知時(shí)間會被放到完成端口隊(duì)列里。反之,則一定有相應(yīng)的通知時(shí)間被放到完成端口隊(duì)列。
更完善的關(guān)于
Winsock的完成端口機(jī)制,可以參考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)有益與對擴(kuò)展性規(guī)則的理解。下圖表示當(dāng)前版本Win2000的Winsock實(shí)現(xiàn)。應(yīng)用程序不應(yīng)該依賴于這里描述的一些底層細(xì)節(jié)(指drivers ,Dlls之類的),因?yàn)檫@些可能會在未來版本的操作系統(tǒng)中被改變。

Socket
體系結(jié)構(gòu)
Winsock2.0
規(guī)范支持多種協(xié)議以及相關(guān)的支持服務(wù)。這些用戶模式服務(wù)支持可以基于其他現(xiàn)存服務(wù)提供者來擴(kuò)展他們自己的功能。比如,一個(gè)代理層服務(wù)支持(LSP)可以把自己安裝在現(xiàn)存的TCP/IP服務(wù)頂層。這樣,代理服務(wù)就可以截取和重定向一個(gè)對底層功能的調(diào)用。
與其他操作系統(tǒng)不同的是,
WinNT和Win2000的傳輸協(xié)議層并不直接給應(yīng)用程序提供socket風(fēng)格的接口,不接受應(yīng)用程序的直接訪問。而是實(shí)現(xiàn)了更多的通用API,稱為傳輸驅(qū)動接口(Transport Driver Interface,TDI).這些API把WinNT的子系統(tǒng)從各種各樣的網(wǎng)絡(luò)編程接口中分離出來。然后,通過Winsock內(nèi)核模式驅(qū)動提供了sockets方法(在AFD.SYS里實(shí)現(xiàn))。這個(gè)驅(qū)動負(fù)責(zé)連接和緩沖管理,對應(yīng)用程序提供socket風(fēng)格的編程接口。AFD.SYS則通過TDI和傳輸協(xié)議驅(qū)動層交流數(shù)據(jù)。
緩沖區(qū)由誰來管理
如上所說,對于使用
socket接口和傳輸協(xié)議層交流的應(yīng)用程序來說,AFD.SYS負(fù)責(zé)緩沖區(qū)的管理。也就是說,當(dāng)一個(gè)程序調(diào)用send或WSASend函數(shù)發(fā)送數(shù)據(jù)的時(shí)候,數(shù)據(jù)被復(fù)制到AFD.SYS的內(nèi)部緩沖里(大小根據(jù)SO_SNDBUF設(shè)置),然后send和WSASend立刻返回。之后數(shù)據(jù)由AFD.SYS負(fù)責(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)從遠(yuǎn)地客戶端接受數(shù)據(jù)的時(shí)候,如果應(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í)候,數(shù)據(jù)即從AFD.SYS的緩沖復(fù)制到應(yīng)用程序提供的緩沖區(qū)里。
在大多數(shù)情況下,這個(gè)體系工作的很好。尤其是應(yīng)用程序使用一般的發(fā)送接受例程不牽涉使用
Overlapped的時(shí)候。開發(fā)人員可以通過使用setsockopt API函數(shù)把SO_SNDBUF和SO_RCVBUF這兩個(gè)設(shè)置的值改為0關(guān)閉AFD.SYS的內(nèi)部緩沖。但是,這樣做會帶來一些后果:
比如,應(yīng)用程序把
SO_SNDBUF設(shè)為0,關(guān)閉了發(fā)送緩沖(指AFD.SYS里的緩沖),并發(fā)出一個(gè)同步堵塞式的發(fā)送操作,應(yīng)用程序提供的數(shù)據(jù)緩沖區(qū)就會被內(nèi)核鎖定,send函數(shù)不會返回,直到連接的另一端收到整個(gè)緩沖區(qū)的數(shù)據(jù)為止。這貌似一種挺不錯的方法,用來判斷是否你的數(shù)據(jù)已經(jīng)被對方全部收取。但實(shí)際上,這是很糟糕的。問題在于:網(wǎng)絡(luò)層即使收到遠(yuǎn)端TCP的確認(rèn),也不能保證數(shù)據(jù)會被安全交到客戶端應(yīng)用程序那里,因?yàn)榭蛻舳丝赡馨l(fā)生“資源不足”等情況,而導(dǎo)致應(yīng)用程序無法從AFD.SYS的內(nèi)部緩沖復(fù)制得到數(shù)據(jù)。而更重大的問題是:由于堵塞,程序在一個(gè)線程里只能進(jìn)行一次send操作,非常的沒有效率。
如果關(guān)閉接受緩沖(設(shè)置
SO_RCVBUF的值為0),也不能真正的提高效率。接受緩沖為0迫使接受的數(shù)據(jù)在比winsock內(nèi)核層更底層的地方被緩沖,同樣在調(diào)用recv的時(shí)候進(jìn)行才進(jìn)行緩沖復(fù)制,這樣你關(guān)閉AFD緩沖的根本意圖(避免緩沖復(fù)制)就落空了。關(guān)閉接收緩沖是沒有必要的,只要應(yīng)用程序經(jīng)常有意識的在一個(gè)連接上調(diào)用重疊WSARecvs操作,這樣就避免了AFD老是要緩沖大量的到來數(shù)據(jù)。
到這里,我們應(yīng)該清楚關(guān)閉緩沖的方法對絕大多數(shù)應(yīng)用程序來說沒有太多好處的了。
然而,一個(gè)高性能的服務(wù)程序可以關(guān)閉發(fā)送緩沖,而不影響性能。這樣的程序必須確保它在同時(shí)執(zhí)行多個(gè)
Overlapped發(fā)送,而不是等待一個(gè)Overlapped發(fā)送結(jié)束之后,才執(zhí)行另一個(gè)。這樣如果一個(gè)數(shù)據(jù)緩沖區(qū)數(shù)據(jù)已經(jīng)被提交,那么傳輸層就可以立刻使用該數(shù)據(jù)緩沖區(qū)。如果程序“串行”的執(zhí)行Overlapped發(fā)送,就會浪費(fèi)一個(gè)發(fā)送提交之后另一個(gè)發(fā)送執(zhí)行之前那段時(shí)間。
資源約束
魯棒性是每一個(gè)服務(wù)程序的一個(gè)主要設(shè)計(jì)目標(biāo)。就是說,服務(wù)程序應(yīng)該可以對付任何的突發(fā)問題,比如,客戶端請求的高峰,可用內(nèi)存的暫時(shí)貧缺,以及其他可靠性問題。為了平和的解決這些問題,開發(fā)人員必須了解典型的
WindowsNT和Windows2000平臺上的資源約束。
最基本的問題是網(wǎng)絡(luò)帶寬。使用
UDP協(xié)議進(jìn)行發(fā)送的服務(wù)程序?qū)Υ艘筝^高,因?yàn)檫@樣的服務(wù)程序要求盡量少的丟包率。即使是使用TCP連接,服務(wù)器也必須注意不要濫用網(wǎng)絡(luò)資源。否則,TCP連接中將會出現(xiàn)大量重發(fā)和連接取消事件。具體的帶寬控制是跟具體程序相關(guān)的,超出了本文的討論范圍。
程序所使用的虛擬內(nèi)存也必須小心。應(yīng)該保守的執(zhí)行內(nèi)存申請和釋放,或許可以使用旁視列表(一個(gè)記錄申請并使用過的“空閑”內(nèi)存的緩沖區(qū))來重用已經(jīng)申請但是被程序使用過,空閑了的內(nèi)存,這樣可以使服務(wù)程序避免過多的反復(fù)申請內(nèi)存,并且保證系統(tǒng)中一直有盡可能多的空余內(nèi)存。(應(yīng)用程序還可以使用
SetWorkingSetSize這個(gè)Win32API函數(shù)來向系統(tǒng)請求增加該程序可用的物理內(nèi)存。)
有兩個(gè)
winsock程序不會直接面對的資源約束。第一個(gè)是頁面鎖定限制。無論應(yīng)用程序發(fā)起send還是receive操作,也不管AFD.SYS的緩沖是否被禁止,數(shù)據(jù)所在的緩沖都會被鎖定在物理內(nèi)存里。因?yàn)閮?nèi)核驅(qū)動要訪問該內(nèi)存的數(shù)據(jù),在訪問期間該內(nèi)存區(qū)域都不能被解鎖。在大部分情況下,這不會產(chǎn)生任何問題。但是操作系統(tǒng)必須確認(rèn)還有可用的可分頁內(nèi)存來提供給其他程序。這樣做的目的是防止一個(gè)有錯誤操作的程序請求鎖定所有的物理RAM,而導(dǎo)致系統(tǒng)崩潰。這意味著,應(yīng)用程序必須有意識的避免導(dǎo)致過多頁面鎖定,使該數(shù)量達(dá)到或超過系統(tǒng)限制。
在
WinNT和Win2000中,系統(tǒng)允許的總共的內(nèi)存鎖定的限制大概是物理內(nèi)存的1/8。這只是粗略的估計(jì),不能作為一個(gè)準(zhǔn)確的計(jì)算數(shù)據(jù)。只是需要知道,有時(shí)重疊IO操作會發(fā)生ERROR_INSUFFICIENT_RESOURCE失敗,這是因?yàn)榭赡芡瑫r(shí)有太多的send/receives操作在進(jìn)行中。程序應(yīng)該注意避免這種情況。
另一個(gè)的資源限制情況是,程序運(yùn)行時(shí),系統(tǒng)達(dá)到非分頁內(nèi)存池的限制。
WinNT和Win2000的驅(qū)動從指定的非分頁內(nèi)存池中申請內(nèi)存。這個(gè)區(qū)域里分配的內(nèi)存不會被扇出,因?yàn)樗硕鄠€(gè)不同的內(nèi)核對象可能需要訪問的數(shù)據(jù),而有些內(nèi)核對象是不能訪問已經(jīng)扇出的內(nèi)存的。一旦系統(tǒng)創(chuàng)建了一個(gè)socket (或打開一個(gè)文件),一定數(shù)目的非分頁內(nèi)存就被分配了。另外,綁定(binding)和連接socket也會導(dǎo)致額外的非分頁內(nèi)存池的分配。更進(jìn)一步的說,一個(gè)I/O請求,比如send或receive,也是分配了很少的一點(diǎn)非分頁內(nèi)存池的(為了跟蹤I/O操作的進(jìn)行,包含必須信息的一個(gè)很小的結(jié)構(gòu)體被分配了)。積少成多,最后還是可能導(dǎo)致問題。因此操作系統(tǒng)限制了非分頁內(nèi)存的數(shù)量。在winNT和win2000平臺上,每個(gè)連接分配的非分頁內(nèi)存的準(zhǔn)確數(shù)量是不相同的,在未來的windows版本上也可能保持差異。如果你想延長你的程序的壽命,就不要打算在你的程序中精確的計(jì)算和控制你的非分頁內(nèi)存的數(shù)量。
雖然不能準(zhǔn)確計(jì)算,但是程序在策略上要注意避免沖擊非分頁限制。當(dāng)系統(tǒng)的非分頁池內(nèi)存枯竭,一個(gè)跟你的程序完全無關(guān)的的驅(qū)動都有可能出問題,因?yàn)樗鼰o法正常的申請到非分頁內(nèi)存。最壞的情況下,會導(dǎo)致整個(gè)系統(tǒng)崩潰。比如那些第三方設(shè)備或系統(tǒng)本身的驅(qū)動。切記:在同一臺計(jì)算機(jī)上,可能還有其他的服務(wù)程序在運(yùn)行,同樣在消耗非分頁內(nèi)存。開發(fā)人員應(yīng)該用最保守的策略估算資源,并基于此策略開發(fā)程序。
資源約束的解決方案是很復(fù)雜的,因?yàn)槭聦?shí)上,當(dāng)資源不足的情況發(fā)生時(shí),可能不會有特定的錯誤代碼返回到程序。程序在調(diào)用函數(shù)時(shí)可能可以得到類似
WSAENOBUFS或
ERROR_INSUFFICIENT_RESOURCES
的這種一般的返回代碼。如何處理這些錯誤呢,首先,合理的增加程序的工作環(huán)境設(shè)置(Working set,如果想獲得更多信息,請參考MSDN里John Robbins關(guān)于 Bugslayer的一章)。如果仍然不能解決問題,那么你可能遇上了非分頁內(nèi)存池限制。那么最好是立刻關(guān)閉部分連接,并期待情況恢復(fù)正常。
關(guān)于接受連接
服務(wù)程序最常做的一個(gè)事情是接受客戶端的連接。
AcceptEx函數(shù)是Winsock API中唯一可以使用重疊IO方式接受Socket連接的函數(shù)。AccpetEx要求一個(gè)傳入一個(gè)socket 作為它的參數(shù)。普通的同步accept函數(shù),新的SOCKET是作為返回值得到的。AcceptEx函數(shù)作為一個(gè)重疊操作,接收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的例程可能是這個(gè)樣子的:
do {
-Wait for a previous AcceptEx to complete //
等待前一個(gè)AcceptEx完成
-Create a new socket and associate it with the completion port //
創(chuàng)建一個(gè)新的Socket并將其關(guān)聯(lián)
//
到完成端口
-Allocate context structure etc. //
初始化相關(guān)的環(huán)境信息結(jié)構(gòu)
-Post an AcceptEx request. //
進(jìn)入AcceptEx請求。
}
while(TRUE);
一個(gè)服務(wù)器一直具備足夠的
AcceptEx調(diào)用,這樣就可以立刻響應(yīng)客戶機(jī)的連接。AcceptEx操作的數(shù)量取決于服務(wù)器的策略。如果要滿足高連接率(比如大量的短暫連接或爆發(fā)性的流量)的話,當(dāng)然比不常發(fā)生連接的程序需要更多的AcceptEx入口。聰明的策略就是根據(jù)流量改變AcceptEx調(diào)用的數(shù)量,而避免只使用一個(gè)確定的數(shù)目。
在
Win2000上,Winsock提供了一些幫助,用來判斷AcceptEx調(diào)用的數(shù)量是否跟不上需要。當(dāng)創(chuàng)建一個(gè)監(jiān)聽Socket之后,使用WSAEventSelect函數(shù)把它和一個(gè)FD_ACCEPT事件關(guān)聯(lián),如果沒有accept未決的調(diào)用正在進(jìn)行,一旦有請求到來,該事件(FD_ACCEPT)就會發(fā)生。因此此事件可以用來告訴開發(fā)人員:還需要進(jìn)行更多的AcceptEx操作,或者由此探測到一個(gè)有異常行為的遠(yuǎn)端實(shí)體。注意:此機(jī)制在NT上是無效的。
使用
AcceptEx的顯著好處是:在一次連接中就可以獲取客戶端的數(shù)據(jù),見AcceptEx的lpOutputBuffer參數(shù)。這意味著如果客戶端連接并立刻發(fā)送數(shù)據(jù)的話,AcceptEx將在客戶端連接成功和數(shù)據(jù)發(fā)送之后才完成。這個(gè)功能同時(shí)導(dǎo)致的問題是:AcceptEx必須等待數(shù)據(jù)接受完成才能返回。因?yàn)橐粋€(gè)帶
Output緩沖的AcceptEx函數(shù)并非一個(gè)“原子”操作,而是兩步的過程:接受連接和等待數(shù)據(jù)。所以程序在數(shù)據(jù)接受之前并不會知道連接成功。當(dāng)然客戶端也可以連接到服務(wù)器而不馬上發(fā)送數(shù)據(jù),如果這樣的連接過多,服務(wù)器將開始拒絕合法的連接,因?yàn)闆]有可用的未決的Accept操作入口。這也是一種常用的方法,通過拒絕訪問,防止惡意攻擊和海量連接。
在正在接受連接的線程中,可以檢查
AcceptEx調(diào)用傳入的socket,調(diào)用getsockopt檢查其SO_CONNECT_TIME,該值返回的是socket連接的時(shí)間,沒有連接的時(shí)候返回-1。
根據(jù)
WSAEventSelect機(jī)制所帶來的特性,我們可以很容易的判斷是否應(yīng)該檢查傳到AcceptEx函數(shù)的socket句柄的連接時(shí)間。如果在一定時(shí)間里,AcceptEx沒有從某個(gè)連接中收到數(shù)據(jù),AcceptEx可以通過關(guān)閉該socket來斷開連接。在不緊急的情況下,程序不應(yīng)該關(guān)閉一個(gè)AcceptEx里處于未連接狀態(tài)的socket ,因?yàn)橄到y(tǒng)考慮到性能問題,關(guān)聯(lián)在AcceptEx上的內(nèi)核態(tài)數(shù)據(jù)結(jié)構(gòu)不會被釋放,直到一個(gè)新的連接到來或監(jiān)聽socket本身都關(guān)閉了。
乍看起來,一個(gè)發(fā)出
AcceptEx請求的線程同時(shí)也可以是一個(gè)關(guān)聯(lián)在完成端口上,并且處理其他完成IO事件的工作線程。然而,最好不要設(shè)計(jì)這樣一個(gè)線程。在winsocket2的層次結(jié)構(gòu)上有一個(gè)副作用,那就是一個(gè)socket/WSASocket API的開銷是相當(dāng)可觀的,每個(gè)AccepEx都需要創(chuàng)建一個(gè)新的socket,所以最好創(chuàng)建一個(gè)單獨(dú)的跟其他IO處理無關(guān)的線程來調(diào)用AcceptEx。當(dāng)然,你還可以利用這個(gè)線程來進(jìn)行其他的工作如創(chuàng)建事件log
關(guān)于
AcceptEx要注意的最后一個(gè)事情是:Winsock2的其他供應(yīng)商不一定會實(shí)現(xiàn)AcceptEx函數(shù)。同樣情況也包括的其他Microsoft的特定APIs如TransmitFile,GetAcceptExSockAddrs以及其他Microsoft將在以后版本的windows里。在運(yùn)行WinNT和Win2000的系統(tǒng)上,這些APIs在Microsoft提供的DLL(mswsock.dll)里實(shí)現(xiàn),可以通過鏈接mswsock.lib或者通過WSAioctl的SIO_GET_EXTENSION_FUNCTION_POINTER操作動態(tài)調(diào)用這些擴(kuò)展APIs.
未獲取函數(shù)指針就調(diào)用函數(shù)(如直接連接
mswsock..lib并直接調(diào)用AcceptEx)的消耗是很大的,因?yàn)?/font>AcceptEx 實(shí)際上是存在于Winsock2結(jié)構(gòu)體系之外的。每次應(yīng)用程序常試在服務(wù)提供層上(mswsock之上)調(diào)用AcceptEx時(shí),都要先通過WSAIoctl獲取該函數(shù)指針。如果要避免這個(gè)很影響性能的操作,應(yīng)用程序最好是直接從服務(wù)提供層通過WSAIoctl先獲取這些APIs的指針。
TransmitFile
和TransmitPackets函數(shù)
Winsock
提供了兩個(gè)專為文件和內(nèi)存數(shù)據(jù)傳輸而優(yōu)化過的函數(shù)。TransmitFile API在WinNT和Win2000均有效,而TransmitPackets作為一個(gè)新的擴(kuò)展函數(shù),將在未來版本的windows里實(shí)現(xiàn)。
TransmitFile
可以把文件的內(nèi)容通過socket傳輸。一般情況下,如果應(yīng)用程序通過socket傳輸文件,首先要用CreateFile打開文件,并循環(huán)調(diào)用ReadFile和WSASend函數(shù),讀取一段數(shù)據(jù)然后發(fā)送,直到整個(gè)文件發(fā)送完畢。這樣的效率很低,因?yàn)?/font>ReadFile和WSASend調(diào)用都需要系統(tǒng)在用戶態(tài)和核心態(tài)之間進(jìn)行轉(zhuǎn)換。TransmitFile則只需要知道需要傳輸?shù)奈募浔鸵獋鬏數(shù)淖止?jié)數(shù),只有CreateFile打開文件獲得句柄這個(gè)向核心態(tài)躍遷的這一個(gè)額外開銷。如果你的程序需要通過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更進(jìn)一步,允許調(diào)用者一次指定多個(gè)文件句柄和內(nèi)存緩沖區(qū),并進(jìn)行傳輸。原形如下:
BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKET_ELEMENT lpPacketArray,
DWORD nElementCount,
DWORD nSendSize,
LPOVERLAPPED lpOverlapped,
DWORD dwFlags
);
lpPacketArray
包含結(jié)構(gòu)體的數(shù)組。每個(gè)入口點(diǎn)指定一個(gè)需要被傳輸?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)體里的元素是一個(gè)文件句柄(TF_ELEMENT_FILE)還是一個(gè)內(nèi)存緩沖(TF_ELEMENT_MEMORY)。cLength成員表示要傳輸?shù)淖止?jié)數(shù)(對于文件句柄,0則表示文件內(nèi)所有的數(shù)據(jù))。一個(gè)未命名的聯(lián)合體(union)包含內(nèi)存緩沖指針或文件句柄(以及指定的偏移量)。
使用這兩個(gè)函數(shù)的其他好處是,你可以通過指定
TF_REUSE_SOCKET標(biāo)志(必須同時(shí)指定TF_DISCONNECT標(biāo)志)重用socket句柄。一旦API函數(shù)完成數(shù)據(jù)傳輸,連接就會在傳輸點(diǎn)的層次上斷開,然后該socket就可以被AcceptEx重新使用。這樣就可以減少反復(fù)創(chuàng)建socket和的次數(shù),優(yōu)化了效率。
使用這兩個(gè)函數(shù)要注意的是,在
WinNT Workstation版本或win2000 Professional版本上,并不能實(shí)現(xiàn)優(yōu)化性能。必須在winNT,win2000 Server ,Win2000 Advanced Server 或Win2000 Data Center版本上才能實(shí)現(xiàn)。
來實(shí)現(xiàn)一個(gè)服務(wù)方案
在前幾章里,我們介紹了一些有益于改善性能提高擴(kuò)展性的
APIs和方法,以及可能遇到的資源瓶頸。這對你有用嗎?當(dāng)然,首先取決于你的服務(wù)端和客戶端的設(shè)計(jì)。在設(shè)計(jì)時(shí),你對服務(wù)端和客戶端的控制越得力,你就能更好的避免瓶頸
讓我們來看一種簡單的用例,在這個(gè)用例里我們設(shè)計(jì)一個(gè)服務(wù)器,這個(gè)服務(wù)器處理客戶端的連接,然后客戶端發(fā)送一次數(shù)據(jù),并期待服務(wù)端的回應(yīng),然后客戶端斷開連接。
我們的設(shè)計(jì)是:服務(wù)器創(chuàng)建一個(gè)監(jiān)聽
socket,并和一個(gè)完成端口相關(guān)聯(lián),然后創(chuàng)建和CPU同等數(shù)量的工作線程,以及一個(gè)專門用來進(jìn)行AcceptEx調(diào)用的線程。既然我們知道客戶端一旦連接馬上就會發(fā)送數(shù)據(jù) ,那么準(zhǔn)備一個(gè)接受緩沖區(qū)會有利于工作的進(jìn)行。當(dāng)然,不要忘記經(jīng)常的檢查正在連接的socket的SO_CONNECT_TIME值,避免死連接。
本設(shè)計(jì)中重要的一項(xiàng)是決定需要顯形調(diào)用多少個(gè)
AcceptEx。因?yàn)槊總€(gè)AcceptEx操作都需要一個(gè)接收緩沖區(qū),大量的頁面將被鎖定(還記得每個(gè)重疊操作都會消耗一些非分頁內(nèi)存,并且會將一些數(shù)據(jù)緩沖鎖定到內(nèi)存里嗎)。沒有公式和具體的準(zhǔn)則指導(dǎo)如何確定究竟允許多少個(gè)AcceptEx操作。最好的方案就是使這個(gè)數(shù)目成為可調(diào)的,通過性能測試,尋找一個(gè)在典型的環(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é)的。
在本例的情況里,不必要去取消每個(gè)
socket的接收緩沖,因?yàn)榻邮帐录H僅在AcceptEx調(diào)用中發(fā)生。保證每個(gè)socket都有一個(gè)接收緩沖不會造成什么危害。一旦客戶端/服務(wù)器在最初的一次請求(由AcceptEx完成)之后進(jìn)行交互,發(fā)送更多的數(shù)據(jù),那么取消接收緩沖更是一個(gè)很不好的做法。除非你能保證這些數(shù)據(jù)都是在每個(gè)連接的重疊IO接收里完成的 。
結(jié)束語:
重復(fù):開發(fā)一個(gè)可擴(kuò)展的
Winsock服務(wù)器并非十分困難的。僅僅是開始一個(gè)監(jiān)聽socket,接收連接,并且進(jìn)行重疊發(fā)送和接收的IO操作。最大的挑戰(zhàn)就是管理系統(tǒng)資源,限制重疊Io的數(shù)量,避免內(nèi)存危機(jī)。遵循這幾個(gè)原則,就能幫助你開發(fā)高性能,可擴(kuò)展的服務(wù)程序。