http://blog.csdn.net/zcq108/article/details/6555167
萬丈高樓平地起,DBIOCP高性能源碼也是由一磚一瓦搭建的,因爲包裝的複雜,設計類比較多,所以一下子難看清楚所有脈絡。
我找到網上一篇最簡單的SOURCE進行一下講解,沒有DELPHI的源碼,是C的。
#include <WINSOCK2.H> //包含的頭文件,相當於DELPHI中的USES winsock單元。
#include <stdio.h>
#define PORT 5150 //端口
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib") //引入SOCKET DLL庫
typedef enum
{
RECV_POSTED
}OPERATION_TYPE;
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
OPERATION_TYPE OperationType;
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
定義一個結構體,我們需要自己的結構體(拓展WSAOVERLAPPED )
DWORD WINAPI WorkerThread(LPVOID);
定義的一個工作線程
int main()
{
WSADATA wsaData; //winsocket 加載時需要的結構
SOCKET sListen, sClient; //偵聽套接字
SOCKADDR_IN local, client;
DWORD i, dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
HANDLE CompletionPort = INVALID_HANDLE_VALUE; //完成端口句柄
SYSTEM_INFO systeminfo;
LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
// 初始化SOCKET 庫
WSAStartup(0x0202, &wsaData);
// 創建一個完成端口(內核對象)
CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 得到系統信息
GetSystemInfo(&systeminfo);
//創建相應的工作隊線程數
for (i = 0; i < systeminfo.dwNumberOfProcessors; i++)
{
CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);
}
//創建偵聽套接字
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 調置要綁定的端口
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// SOCKET偵聽
listen(sListen, 3);
//主線程做一個循環,接受新的客戶端連接。
while (TRUE)
{
// Accept 等待一個客戶端連接
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d/n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// 新的客戶端與完成端口綁定(注意第三個參數)
CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)sClient, 0);
// 創建一個結構體, 這裏的結構體的作用是要交給完成端口,說到這裏,你就明白爲什麼要定義
一個結構了,這個結構必須是WSAOVERLAPPED 的拓展。
lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = RECV_POSTED;
//投遞一個RECV事件
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
//投遞退出事件
PostQueuedCompletionStatus(CompletionPort, 0xFFFFFFFF, 0, NULL);
CloseHandle(CompletionPort);
closesocket(sListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort=(HANDLE)CompletionPortID;
DWORD dwBytesTransferred;
SOCKET sClient;
LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
while (TRUE)
{
//得到一個事件
GetQueuedCompletionStatus(
CompletionPort,
&dwBytesTransferred,
&sClient,
(LPOVERLAPPED *)&lpPerIOData,
INFINITE);
//如果是PostQueuedCompletionStatus事件,則退出線程
if (dwBytesTransferred == 0xFFFFFFFF)
{
return 0;
}
//RECV事件
if (lpPerIOData->OperationType == RECV_POSTED)
{
//說明客戶端斷開
if (dwBytesTransferred == 0)
{
// Connection was closed by client
closesocket(sClient);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
//收到客戶端消息之後,發送數據給客戶端
lpPerIOData->szMessage[dwBytesTransferred] = '/0';
send(sClient, lpPerIOData->szMessage, dwBytesTransferred, 0);
// 再接受客戶端下一個發送的數據
memset(lpPerIOData, 0, sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = RECV_POSTED;
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
}
}
return 0;
}
http://blog.csdn.net/zcq108/article/details/6555172
本文力求用簡潔的方式,來解釋IOCP的運作。
原理方面,網上有比較多的講解,但我的看法是,太書面化,
太理論化。IOCP其實是一項並不難掌握的技術,之所以難,在於
設計的理念和實際應用場合不同,還有就是書中的作者,大部分沒有
經過實際項目的開發。
1. 完成端口
剛聽到的時候,這個名字很陌生,也很唬人,它其實是一種內核對象。
不要被它的名字嚇倒。
2. 異步
如果你要了解完成端口,還是先了解一下異步I/O的工作原理,並於異步
最簡單的說法,當你一件事情處理需要一定時間,你不想等待它完成之後
再做下一步動作,你用異步的方式,在處理完的時候,讓系統告訴你。
3. 隊列
完成端口和隊列是緊密相關的,你認識不到這一點,你永遠都別想理解IOCP
對於每個I/O的投遞,其實內部的操作已經進入了完成端口隊列。
5. 線程池
準確的說,是你創建了多個線程,因爲是IOCP幫助你調度的,所以可以理解爲
IOCP線程池
6. FIFO
隊列的方式,是先進先出的方式,這個你必須要記住
7. 後進先出
IOCP線程池的調度原理其實是後進先出的方式。這點,不容易理解,不過你一定
要記得和FIFO區分開。這裏的線程池調度,是指線程被激活的這種方式。或許說
線程切換的方式。
8. 事件通知
完成端口沒什麼祕密,它一樣是事件通知的方式。不要感到任何的奇怪。
以上8點,你應該確認在你理解了的情況下,再去讀完成端口源碼,否則,你讀了,也讀不懂。
IOCP 完整
在你開發不同類型的軟件,不久之後或者更晚,你必須得面對客戶端/服務器端的發展。對程序員來說,寫一個全面的客戶端/服務器的代碼是很困難的。這篇文章提供了一個簡單的,但卻強大的客戶端/服務器源代碼,它能夠被擴展到許多客戶端/服務器的應用程序中。源代碼使用高級的IOCP技術,這種技術能高效的爲多個客戶端提供服務。IOCP技術提供了一種對 一個線程—一個客戶端(one-thread-one client)這種瓶頸問題(很多中問題的一個)的有效解決方案。它使用很少的一直運行的線程和異步輸入/輸出,發送/接收。IOCP技術被廣泛應用於各自高性能的服務器,像Apache等。源代碼也提供了一系列的函數,在處理通信、客戶端/服務器接收/發送文件函數、還有線程池處理等方面都會經常用到。文章主要關注利用IOCP應用API函數的實際解決方案,也提供了一個全面的代碼文檔。此外,也爲你呈現了一個能處理多個連接、同時能夠進行文件傳輸的簡單回覆客戶端/服務器。
2.1. 介紹:
這片文章提供了一個類,它是一個應用於客戶端和服務器的源代碼,這個類使用IOCP和異步函數,我們稍後會進行介紹。這個源代碼是根據很多代碼和文章得到的。
利用這些簡單的源代碼,你能夠:
l 服務/連接多個客戶端和服務器。
l 異步發送和接收文件。
l 爲了處理沉重的客戶端/服務器請求,創建並管理一個邏輯工作者線程池。(logical worker thread pool)。
我們很難找到充分的,但簡單的能夠應對客戶端/服務器通信的源代碼。在網上發現的源代碼即複雜(超過20個類),又不能提供足夠的功能。本問的代碼儘量簡單,也有好的文檔。我們將簡要介紹Winsock API 2.0提供的IOCP技術,編碼時遇到的疑難問題,以及這些問題的應對方案。
2.2. 異步輸入/輸出完成端口(IOCP)簡介
一個服務器應用程序,假如不能夠同時爲多個客戶端提供服務,那它就沒有什麼意義。通常的異步I/O調用,還有多線程都是這個目的。準確的說,一個異步I/O調用能夠立即返回,儘管有阻塞的I/O調用。同時,I/O異步調用的結果必須和主線程同步。這可以用很多種方法實現,同步可以通過下面方法實現:
l 利用事件——當異步調用完成時設定的信號。這種方法的優點是線程必須檢查和等待這個信號被設定。
l 使用GetOverlappedResult函數——這個方法和上面方法有相同的優點。
l 使用異步程序調用(APC)——這種方法有些缺點。第一,APC總是在正被調用的線程的上下文中被調用;第二,調用線程必須暫停,等待狀態的改變。
l 使用IOCP——這種方法的缺點是有些疑難問題必須解決。使用IOCP編碼多少有些挑戰。
2.2.1 爲什麼使用IOCP
使用IOCP,我們能夠克服 一個線程 —— 一個客戶端 問題。我們知道,假如軟件不是運行在一個真實的多處理器機器上,它的性能會嚴重下降。線程是系統的資源,它們即不是無限的,也不便宜。
IOCP提供了一種利用有限的(I/O工作線程)公平的處理多客戶端的輸入/輸出問題的解決辦法。線程並不被阻塞,在無事可作的情況下也不使CPU循環。
2.3. 什麼是IOCP
我們已經知道,IOCP僅僅是一個線程同步對象,有點像信號量(semaphore),因此IOCP並不是一個難懂的概念。一個IOCP對象和很多支持異步I/O調用的I/O對象相聯繫。線程有權阻塞IOCP對象,直到異步I/O調用完成。
3 IOCP如何工作
爲了得到更多信息,建議你參考其它的文章(1, 2, 3, see References)。
使用IOCP,你必須處理3件事情。將一個套接字綁定到一個完成端口,使用異步I/O調用,和使線程同步。爲了從異步I/O調用得到結果,並知道一些事情,像哪個客戶端進行的調用,我們必須傳遞兩個參數:CompletionKey參數,還有OVERLAPPED結構體。
3.1. CompletionKey參數
CompletionKey參數是第一個參數,是一個DWORD類型的變量。你可以給它傳遞你想要的任何值,這些值總是和這個參數聯繫。通常,指向結構體的指針,或者包含客戶端指定對象的類的指針被傳遞給這個參數。在本文的源代碼中,一個ClientContext結構體的指針被傳遞給CompletionKey參數。
3.2. OVERLAPPED參數
這個參數通常被用來傳遞被異步I/O調用的內存。要重點強調的是,這個數據要被加鎖,並且不要超出物理內存頁,我們之後進行討論。
3.3. 將套接字和完成端口進行綁定
一旦創建了完成端口,通過調用CreateIoCompletionPort函數可以將一個套接字和完成端口進行綁定,像下面的方法:
BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket,
HANDLE hCompletionPort, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort((HANDLE) socket,
hCompletionPort, dwCompletionKey, m_nIOWorkers);
return h == hCompletionPort;
}
3.4. 進行異步I/O調用
通過調用WSASend, WSARecv函數,進行實際的異步調用。這些函數也需要包含將要被用到的內存指針的參數WSABUF。通常情況下,當服務器/客戶端想要執行一個I/O調用操作,它們並不直接去做,而是發送到完成端口,這些操作被I/O工作線程執行。這是因爲,要公平的分配CPU。通過給完成端口傳遞一個狀態,進行I/O調用。象下面這樣:
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,
pOverlapBuff->GetUsed(),
(DWORD) pContext, &pOverlapBuff->m_ol);
3.5. 線程的同步
通過調用GetQueuedCompletionStatus函數進行線程的同步(看下面)。這個函數也提供了CompletionKey 參數 OVERLAPPED參數。
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // handle to completion port
LPDWORD lpNumberOfBytes, // bytes transferred
PULONG_PTR lpCompletionKey, // file completion key
LPOVERLAPPED *lpOverlapped, // buffer
DWORD dwMilliseconds // optional timeout value
);
3.6. 四個棘手的IOCP編碼問題和它們的對策
使用IOCP會遇到一些問題,有些問題並不直觀。在使用IOCP的多線程場景中,並不直接控制線程流,這是因爲線程和通信之間並沒有聯繫。在這部分,我們將提出四個不同的問題,在使用IOCP開發客戶端/服務器程序時會遇到它們。它們是:
l WSAENOBUFS出錯問題。
l 數據包的重排序問題。
l 訪問紊亂(access violation)問題
3.6.1 WSAENOBUFS出錯問題。
這個問題並不直觀,並且很難檢查。因爲,乍一看,它很像普通的死鎖,或者內存泄露。假設你已經弄好了你的服務器並且能夠很好的運行。當你對服務器進行承受力測試的時候,它突然掛機了。如果你幸運,你會發現這和WSAENOBUFS出錯有關。
伴隨着每一次的重疊發送和接收操作,有數據的內存提交可能會被加鎖。當內存被鎖定時,它不能越過物理內存頁。操作系統會強行爲能夠被鎖定的內存的大小設定一個上限。當達到上限時,重疊操作將失敗,併發送WSAENOBUFS錯誤。
假如一個服務器在在每個連接上提供了很多重疊接收,隨着連接數量的增長,很快就會達到這個極限。如果服務器能夠預計到要處理相當多的併發客戶端的話,服務器可以在每個連接上僅僅回覆一個0字節的接收。這是因爲沒有接收操作和內存無關,內存不需要被鎖定。利用這個方法,每一個套接字的接收內存都應該被完整的保留,這是因爲,一旦0字節的接收操作完成,服務器僅僅爲套接字的接收內存的所以數據內存返回一個非阻塞的接收。利用WSAEWOULDBLOCK,當非阻塞接收失敗時,也沒有數據被阻塞。這種設計的目的是,在犧牲數據吞吐量的情況下,能夠處理最大量的併發連接。當然,對於客戶端如何和服務器交互,你知道的越多越好。在以前的例子中,每當0字節的接收完成,返回存儲了的數據,馬上執行非阻塞接收。假如服務器知道客戶端突然發送數據,當0字節接收一旦完成,爲防止客戶端發送一定數量的數據(大於每個套接字默認的8K內存大小),它可以投遞一個或多個重疊接收。
源代碼提供了一個簡單的解決WSAENOBUFS錯誤的可行方案。對於0字節內存,我們採用WSARead()函數(見OnZeroByteRead())。當調用完成,我們知道數據在TCP/IP棧中,通過採用幾個異步WSARead()函數讀取MAXIMUMPACKAGESIZE的內存。這個方法在數據達到時僅僅鎖定物理內存,解決了WSAENOBUFS問題。但是這個方案降低了服務器的吞吐量(見第9部分的Q6和A6例子)。
3.6.2 數據包的重排序問題
在參考文獻3中也討論了這個問題。儘管使用IOCP,可以使數據按照它們被髮送的順序被可靠的處理,但是線程表的結果是實際工作線程的完成順序是不確定的。例如,假如你有兩個I/O工作線程,並且你應該接收“字節數據塊1、字節數據塊2 、字節數據塊3”,你可以按照錯誤的順序處理它們,也就是“字節數據塊2、字節數據塊1 、字節數據塊3”。這也意味着,當你通過把發送請求投遞到IO完成端口來發送數據時,數據實際上是被重新排序後發送的。
這個問題的一個實際解決辦法是,爲我們的內存類增加順序號,並按照順序號處理內存。意思是,具有不正確號的內存被保存備用,並且因爲性能原因,我們將內存保存在希哈表中(例如m_SendBufferMap和m_ReadBufferMap)。
要想得到更多這個方案的信息,請查看源代碼,並在IOCPS類中查看下面的函數:
l GetNextSendBuffer (..) 和GetNextReadBuffer(..), 爲了得到排序的發送或接收內存。
l IncreaseReadSequenceNumber(..)和IncreaseSendSequenceNumber(..), 爲了增加順序號。
3.6.3 異步阻塞讀和字節塊包處理問題
大多數服務器協議是一個包,這個包的基礎是第一個X位的描述頭,它包含了完整包的長度等詳細信息。服務器可以解讀這個頭,可以算出還需要多少數據,並一直解讀,直到得到一個完整的包。在一個時間段內,服務器通過異步讀取調用是很好的。但是,假若我們想全部利用IOCP服務器的潛力,我們應該有很多的異步讀操作等待數據的到達。意思是很多異步讀無順序完成(像在3.6.2討論的),通過異步讀操作無序的返回字節塊流。還有,一個字節塊流(byte chunk streams)能包含一個或多個包,或者包的一部分,如圖1所示:
圖1
這個圖表明部分包(綠色)和完整的包(黃色)在字節塊流中是如何異步到達的。
這意味着我們要想成功解讀一個完整包,必須處理字節流數據塊(byte stream chunks)。還有,我們必須處理部分包,這使得字節塊包的處理更加困難。完整的方案可以在IOCP類裏的ProcessPackage(..)函數中找到。
3.6.4 訪問紊亂(access violation)問題。
這是一個次要問題,是編碼設計的結果,而不是IOCP的特有問題。倘若客戶端連接丟失,並且一個I/O調用返回了一個錯誤標識,這樣我們知道客戶端已經不在了。在CompletionKey參數中,我們爲它傳遞一個包含了客戶端特定數據的結構體的指針。假如我們釋放被ClientContext結構體佔用的內存,被同一個客戶端執行I/O調用所返回的錯誤碼,我們爲ClientContext指針傳遞雙字節的CompletionKey變量,試圖訪問或刪除CompletionKey參數,這些情況下會發生什麼?一個訪問紊亂髮生了。
這個問題的解決辦法是爲ClientContext結構體增加一個阻塞I/O調用的計數(m_nNumberOfPendlingIO),當我們知道沒有阻塞I/O調用時我們刪除這個結構體。EnterIoLoop(..) 函數和 ReleaseClientContext(..).函數就是這樣做的。
3.7 源代碼總攬
源代碼的目標是提供一些能處理與IOCP有關的問題的代碼。源代碼也提供了一些函數,它們在處理通信、客戶端/服務器接收/發送文件函數、還有線程池處理等方面會經常用到。
圖2 源代碼IOCPS類函數總攬
我們有很多I/O工作線程,它們通過完成端口(IOCP)處理異步I/O調用,這些工作線程調用一些能把需要大量計算的請求放到一個工作隊列着中的虛函數。邏輯工作線程從隊列中渠道任務,進行處理,並通過使用一些類提供的函數將結果返回。圖形用戶界面(GUI)通常使用Windows消息,通過函數調用,或者使用共享的變量,和主要類進行通信。
圖3
圖3顯示了類的總攬。
圖3中的類歸納如下:
l CIOCPBuffer:管理被異步I/O調用使用的內存的類。
l IOCPS:處理所有通信的主要類。
l JobItem:包含被邏輯工作線程所執行工作的結構體。
l ClientContext:保存客戶端特定信息的結構體(例如:狀態、數據 )。
3.7.1 內存設計——CIOCPBuffer類
當使用異步I/O調用時,我們必須爲I/O操作提供一個私有內存空間。當我們分配內存時要考慮下面一些情況:
l 分配和釋放內存是很費時間的,因此我們要反覆利用分配好的內存。所以,我們像下面所示將內存保存在一個連接表中。
· // Free Buffer List..
·
· CCriticalSection m_FreeBufferListLock;
· CPtrList m_FreeBufferList;
· // OccupiedBuffer List.. (Buffers that is currently used)
· CCriticalSection m_BufferListLock;
· CPtrList m_BufferList;
· // Now we use the function AllocateBuffer(..)
·
// to allocate memory or reuse a buffer.
l 有時,當一個異步I/O調用完成時,我們可能在內存中有部分包,因此我們爲了得到一個完整的消息,需要分離內存。在CIOCPS類中的函數SplitBuffer()可以實現這一目標。我們有時也需要在兩個內存間複製信息, CIOCPS類中的AddAndFlush()函數可以實現。
l 我們知道,我們爲我們的內存增加序列號和狀態變量(IOZeroReadCompleted())。
l 我們也需要字節流和數據相互轉換的方法,在CIOCPBuffer類中提供了這些函數。
在我們的CIOCPBuffer類中,有上面所有問題的解決辦法。
3.8 如何使用源代碼
從IOCP中派生你自己的類,使用虛函數,使用IOCPS類提供的函數(例如:線程池)。使用線程池,通過使用少數的線程,爲你爲各種服務器或客戶端高效的管理大量的連接提供了可能。
3.8.1 啓動和關閉服務器/客戶端
啓動服務器,調用下面的函數:
BOOL Start(int nPort=999,int iMaxNumConnections=1201,
int iMaxIOWorkers=1,int nOfWorkers=1,
int iMaxNumberOfFreeBuffer=0,
int iMaxNumberOfFreeContext=0,
BOOL bOrderedSend=TRUE,
BOOL bOrderedRead=TRUE,
int iNumberOfPendlingReads=4);
l nPortt :服務器將監聽的端口號(在客戶端模式設爲-1)。
l iMaxNumConnections:最多允許連接數。
l iMaxIOWorkers :輸入/輸出工作線程數。
l nOfWorkers:邏輯工作者數(在運行時能被改變)。
l iMaxNumberOfFreeBuffer :保留的重複利用的內存的最大數量(-1:無 ,0:無窮)。
l iMaxNumberOfFreeContext :保留的重複利用的客戶端信息的最大數量(-1:無 ,0:無窮)。
l bOrderedRead :用來進行順序讀。
l bOrderedSend :用來進行順序發送。
l iNumberOfPendlingReads :等待數據的異步讀循環的數量。在連接到一個遠端的連接時調用下面的函數:
Connect(const CString &strIPAddr, int nPort)
l strIPAddr :遠端服務器的IP地址。
l nPort:端口。
關閉服務器,調用函數:ShutDown()。
例如:
MyIOCP m_iocp;
if(!m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("Error could not start the Client");
….
m_iocp.ShutDown();
5.1 文件傳輸
使用Winsock 2.0的TransmitFile 函數傳輸文件。TransmitFile 函數在連接的套接字句柄上傳輸文件數據。此函數使用操作系統的緩衝管理機制接收文件數據,在套接字上提供高性能的文件傳輸。在異步文件傳輸上有以下幾個重要方面:
l 除非TransmitFile 函數返回,否則不能再對套接字執行 發送 或 寫入 操作,不然會破壞文件的傳輸。在執行PrepareSendFile(..) 函數後,所有對ASend函數的調用都是不允許的。
l 由於系統是連續讀文件數據,打開文件句柄的FILE_FLAG_SEQUENTIAL_SCAN特性可以提高緩存性能。
l 在發送文件(TF_USE_KERNEL_APC)時,我們使用內核的異步程序調用。TF_USE_KERNEL_APC的使用可以帶來明顯的性能提升。很可能(儘管不一定),帶有TransmitFile 的線程的上下文環境的初始化會有沉重的計算負擔;這種情況下可以防止反覆執行APC(異步程序調用)。
文件傳輸的順序如下:服務器通過調用PrepareSendFile(..)函數初始化文件傳輸。客戶端接收到文件信息時,通過調用PrepareReceiveFile(..)函數準備接收,並且給服務器發送一個包來開始文件傳輸。在服務器收到包後,它調用使用高性能的TransmitFile函數的StartSendFile(..)函數傳輸指定的文件。
6 源代碼例子
提供的源代碼是一個模擬客戶端/服務器的例子,它也提供了文件傳輸功能。在源碼中,從類IOCP派生出的類MyIOCP處理客戶端和服務器端的通信。在4.1.1 部分提到了這個虛函數的用法。
在客戶端,或者服務器端的代碼中,虛函數NotifyReceivedPackage是重點。描述如下:
void MyIOCP::NotifyReceivedPackage(CIOCPBuffer *pOverlapBuff,
int nSize,ClientContext *pContext)
{
BYTE PackageType=pOverlapBuff->GetPackageType();
switch (PackageType)
{
case Job_SendText2Client :
Packagetext(pOverlapBuff,nSize,pContext);
break;
case Job_SendFileInfo :
PackageFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_StartFileTransfer:
PackageStartFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_AbortFileTransfer:
DisableSendFile(pContext);
break;};
}
這個函數處理進來的消息和遠程連接發送的請求。在這種情形下,它只不過進行一個簡單的回覆或者傳輸文件。源代碼分爲兩部分,IOCP和IOCPClient,
它們是連接的雙方。 6.1 編譯器問題
在使用VC++ 6.0 或者 .NT時,在處理類CFile時可能會出現一些奇怪的錯誤。像下面這樣:
“if (pContext->m_File.m_hFile !=
INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "
"from 'void *' to 'unsigned int'”
在你更新頭文件(*.h),或者更新你的VC++ 6.0版本後,或者只是改變類型轉換錯誤,都可能會解決這些問題。經過一些修改,這個客戶端/服務器的源代碼在沒有MFC的情況下也能使用。 7 注意點和解決規則
在你將此代碼用於其它類型的程序時,有一些編程的陷阱和源代碼有關,使用“多線程編程”可以避免。不確定的錯誤是那些隨時發生的錯誤,並且通過執行相同的出錯的任務的順序這種方式很難降低這些不確定的錯誤。這類錯誤是存在的最嚴重的錯誤,一般情況下,它們出錯是因爲源代碼設計執行的內核的出錯上。當服務器運行多個IO工作線程時,爲連接的客戶端服務,假如編程人員沒有考慮源代碼的多線程環境,就可能會發生像違反權限這種不確定的錯誤。
解決規則 #1:
像下面例子那樣,絕不在使用上下文 “鎖”之前鎖定客戶端的上下文(例如ClientContext)之前進行讀/寫。通知函數(像:Notify*(ClientContext *pContext))已經是“線程安全的”,你訪問ClientContext的成員函數,而不考慮上下文的加鎖和解鎖。
//Do not do it in this way
// …
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
// …
// Do it in this way.
//….
pContext->m_ContextLock.Lock();
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
pContext->m_ContextLock.Unlock();
//…
當然,你要明白,當你鎖定一個上下文時,其他的線程或GUI都將等待它。
解決規則 #2:
要避免,或者“特別注意”使用那些有複雜的“上下文鎖”,或在一個“上下文鎖”中有其他類型的鎖的代碼。因爲它們很容易導致“死鎖”。(例如:A等待B,B等待C,而C等待A => 死鎖)。
pContext-> m_ContextLock.Lock();
//… code code ..
pContext2-> m_ContextLock.Lock();
// code code..
pContext2-> m_ContextLock.Unlock();
// code code..
pContext-> m_ContextLock.Unlock();
上面的代碼可以導致一個死鎖。
解決規則 #3:
絕不要在通知函數(像Notify*(ClientContext *pContext))的外面訪問一個客戶端的上下文。假如你必須這樣做,務必使用m_ContextMapLock.Lock(); … m_ContextMapLock.Unlock()對它進行封裝。如下面代碼所示:
ClientContext* pContext=NULL ;
m_ContextMapLock.Lock();
pContext = FindClient(ClientID);
// safe to access pContext, if it is not NULL
// and are Locked (Rule of thumbs#1:)
//code .. code..
m_ContextMapLock.Unlock();
// Here pContext can suddenly disappear because of disconnect.
// do not access pContext members here.
8 下一步的工作
下一步,代碼會被更新,在時間順序上會具有下面的特性:
1. 可以接受新的連接的AcceptEx(..)函數的應用將添加到源代碼中,用來處理短時的連接爆炸(short lived connection bursts)和DOS攻擊。
2. 源代碼可以很容易的用於其它平臺,像Win32, STL, 和 WTL。
說明:最近比較忙,各種事情應接不暇。終於弄完了,呵呵。
源代碼可以到網上下載,我分析了,很好,可以應用到我的項目中,嘿嘿。
出處 http://www.codeproject.com/KB/IP/iocp_server_client.aspx