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