“完成端口”模型是迄今爲止最爲複雜的一種I/O模型。然而,假若一個應用程序同時需要管理爲數衆多的套接字,那麼採用這種模型,往往可以達到最佳的系統性能!但不幸的是,該模型只適用於Windows NT和Windows 2000操作系統。因其設計的複雜性,只有在你的應用程序需要同時管理數百乃至上千個套接字的時候,而且希望隨着系統內安裝的CPU數量的增多,應用程序的性能也可以線性提升,才應考慮採用“完成端口”模型。要記住的一個基本準則是,假如要爲Windows NT或Windows 2000開發高性能的服務器應用,同時希望爲大量套接字I/O請求提供服務(Web服務器便是這方面的典型例子),那麼I/O完成端口模型便是最佳選擇!
從本質上說,完成端口模型要求創建一個windows完成端口對象,該對象通過指定數量的線程,對重疊I/O進行管理,以便爲已完成的重疊I/O請求提供服務。要注意的是,所謂完成端口,實際上是windows採用的一種I/O構造機制,除套接字句柄之外,還可以接受其他東西。
使用這種模型之前,首先要創建一個I/O完成端口對象,用它面向任意數量的套接字句柄,管理多個I/O請求,要做到這一點,首先調用函數:
- HANDLE CreateIoCompletionPort(
- HANDLE FileHandle,
- HANDLE ExistingCompletionPort,
- DWORD CompletionKey,
- DWORD NumberOfConcurrentThreads
- );
首先注意該函數實際用於兩個截然不同的兩個目的:
1.用於創建一個完成端口對象
2.將一個句柄同完成端口關聯在一起
最開始創建完成端口時,我們唯一感興趣的是NumberOfConccurrentThreads,前三個參數不太重要。 NumberOfConccurrentThreads定義了在一個完成端口上,同時允許執行的線程數量。若將該參數設爲0,則告訴系統安裝了多少個處理器,則允許同時運行多少個線程,可用如下代碼創建一個I/O完成端口:
- CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
該語句的作用是返回一個句柄,在爲完成端口分配了一個套接字句柄後,用來對那個端口進行標識。
工作器線程與完成端口
成功創建一個完成端口後,便可開始將套接字句柄與對象關聯到一起。但在關聯套接字之前,首先必須創建一個或多個工作器線程,以便在套接字的I/O請求投遞給完成端口後,爲完成端口提供服務。
假如事先預計到線程有可能暫時處於阻塞狀態,那麼最好能夠創建比CreateIoCompletionPort的NumberOfConccurrentThreads值更多的線程。以便到時候充分發揮系統的潛力。
一旦在完成端口上擁有足夠多的工作器線程來爲I/O請求提供服務,便可着手將套接字句柄同完成端口關聯在一起。需要在一個完成端口上調用CreateIoCompletionPort函數,同時爲前三個參數FileHandle,ExistingCompletionPort和CompletionKey提供套接字信息。其中,FileHandle參數指定一個要同完成端口關聯在一起的套接字句柄,ExistingCompletionPort參數標識的是一個現有的完成端口套接字句柄已經與他關聯在一起。CompletionKey參數標識的是要與某個特定套接字句柄關聯在一起的單句柄數據;在這個參數中,應用程序可保持與一個套接字對應的任意類型信息。之所以叫它單句柄數據,是由於它代表了與套接字句柄關聯在一起的數據。可將它作爲指向一個數據結構的指針;在這個結構中,同時包含了套接字的句柄,以及與該套接字有關的其他信息。爲完成端口提供服務的線程的例程可通過這個參數,取得與套接字句柄有關的信息。
下面示例闡述瞭如何使用完成端口模型,來開發一個迴應服務器應用程序,這個程序基本按照如下步驟進行:
1.創建一個完成端口,第四個參數爲0,它指定完成端口上每個處理器一次只允許執行一個工作器線程
2.判斷系統內有多少個處理器
3.創建工作器線程,根據步驟2得到的處理器信息,在完成端口上爲已完成的I/O請求提供服務。在這個簡單的例子中,我們爲每個處理器只創建一個工作器線程。調用CreateThread函數時,必須同時提供一個工作器例程,由線程在創建好後執行
4.準備好一個監聽套接字,在端口上監聽傳入的連接
5.使用accept接收入站的連接請求
6.創建一個數據結構,用於容納單句柄數據,同時在結構中存入接收的套接字句柄
7.調用CreateIoCompletionPort,將自accept返回的新套接字句柄同完成端口關聯在一起。通過 CompletionKey 參數,將單句柄數據結構傳遞給CreateIoCompletionPort
8.開始在已結束的連接上進行I/O操作,在此,我們希望通過重疊I/O機制,在新建套接字投遞一個或多個WSARecv或WSASend請求。這些I/O請求完成後,工作器線程會爲I/O請求提供服務,同時繼續處理以後的I/O請求
9.重複步驟5~8,直到服務器終止
- HANDLE CompletionPort;
- WSADATA wsd;
- SYSTEM_INFO SystemInfo;
- SOCKADDR_IN addr;
- SOCKET Listen;
- int i;
- typedef struct _PER_HANDLE_DATA
- {
- SOCKET Socket;
- SOCKADDR_STORAGE ClientAddr;
- //將和這個句柄關聯的其他信息
- }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
- //加載Winsock
- StartWinsock(MAKEWORD(2,2), &wsd);
- //第一步
- //創建一個I/O完成端口
- CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
- //第二步
- //確定系統有多少個處理器
- GetSystemInfo(&SystemInfo);
- //第三步
- //基於系統中可用的處理器數量創建工作器線程
- //對這個例子,爲每個處理器創建一個工作器線程
- for(i=0; i<SystemInfo.dwNumberOfProcessors; i++)
- {
- HANDLE ThreadHandle;
- //創建一個服務器的工作線程,並將完成端口傳遞到該線程
- ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread,
- CompletionPort, 0, NULL);
- //關閉線程句柄
- CloseHandle(ThreadHandle);
- }
- //第四步
- //創建一個監聽套接字
- Listen = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
- addr.sin_family = AF_INET;
- addr.sin_port = htons(5050);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(Listen, (PSOCKADDR)&addr, sizeof(SOCKADDR_IN));
- listen(Listen, 5);
- while(TRUE)
- {
- PER_HANDLE_DATA *PerHandleData = NULL;
- SOCKADDR_IN saRemote;
- SOCKET Accept;
- int RemoteLen;
- //第五步
- //接收連接,並分配到完成端口
- RemoteLen = sizeof(SOCKADDR_IN);
- Accept = WSAAccept(Listen, (SOCKADDR*)&saRemote, &RemoteLen);
- //第六步
- //創建用來和套接字關聯的單句柄數據信息結構
- PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR,sizeof(PER_HANDLE_DATA));
- printf("Socket Number %d connected\n",Accept);
- PerHandleData->Socket = Accept;
- memcpy(&PerHandleData->ClientAddr,&saRemote,RemoteLen);
- //第七步
- //將接收套接字和完成端口關聯起來
- CreateIoCompletionPort((HANDLE)Accept,
- CompletionPort,
- (DWORD)PerHandleData,
- 0);
- //第八步
- //開始在接受套接字上處理I/O
- //使用重疊I/O,在套接字上投遞一個或多個WSASend或WSARecv調用
- WSARecv(...);
- }
- DWORD WINAPI ServerWorkerThread(LPVOID lpParam)
- {
- //工作器線程
- return 0;
- }
完成端口和重疊I/O
將套接字句柄與一個完成端口關聯在一起後,便能以套接字句柄爲基礎,投遞重疊發送與接收請求,開始對I/O請求進行處理,之後可開始依賴完成端口,接收有關I/O操作完成情況通知。從本質上說,完成端口模型利用了Windows重疊I/O機制。在這種機制中,類似WSASend和WSARecv這樣的WindowsAPI調用會立即返回。此時,需要由應用程序負責在以後的某個時間,通過OVERLAPPED結構來檢索調用的結果。在完成端口模型中,想要做到這一點需要使用GetQueuedCompletionStatus函數,讓一個或多個工作器線程在完成端口上等待:
- BOOL GetQueuedCompletionStatus(
- HANDLE CompletionPort,
- LPWORD lpNumberOfBytesTransferred,
- PULONG_PTR lpCompletionkey,
- LPOVERLAPPED * lpOverlapped,
- DWORD dwMilliseconds
- );
其中,CompletionPort對應與線程所在的完成端口。lpNumberOfBytesTransferred參數負責在完成一次I/O操作後,接收實際傳輸的字節數。lpCompletionkey參數爲原先傳遞到CreateIoCompletionPort函數的套接字返回單句柄數據。如前所述,大家最好將套接字句柄保持在這個鍵中。lpOverlapped參數用於接收已完成的I/O操作的WSAOVERLAPPED結構。因爲可用它獲取每個I/O操作的數據,所有這實際上也是一個相當重要的參數。dwMilliseconds用於指明調用者等待一個完成數據包在完成端口上出現時,希望等候的毫秒數。假如將其設爲INFINITE,調用會無休止的等待下去。
單句柄數據和單I/O操作數據
當一個工作器線程從GetQueuedCompletionStatus這個API調用中接收到I/O完成通知後,在lpCompletionKey和lpOverlapped參數中,會包含一些必要的套接字信息。利用這些信息,可通過完成端口,繼續在一個套接字上進行I/O處理。通過這些參數,可獲得兩種重要的套接字數據類型:單句柄數據和單I/O操作數據。
因爲在一個套接字首次與完成端口關聯到一起的時候,單句柄數據便與一個特定的套接字句柄對應起來了,所有lpCompletionKey參數也包含了單句柄數據。這些數據真是在進行CreateIoCompletionPort調用的時候,通過CompletionKey參數傳遞的。通常情況下,應用程序會將與I/O請求有關的套接字句柄保存在這裏。
lpOverlappde則包含了一個OVERLAPPED結構,在它後面跟隨單I/O操作數據。工作器線程處理一個完成數據包時(迴應數據,接受連接以及投遞另一個線程等),這些信息是它必須知道的。單I/O操作數據是包含在一個結構內的,任意數量的字節,這個結果本身也包含了一個OVERLAPPED結構,假如一個函數要求用到一個OVERLAPPED結構,我們便必須將這樣的一個結構傳遞進去,以滿足它的要求。要想做到這一點,一個簡單的方法是定義一個結構,然後將OVERLAPPED結構作爲新結構的第一個元素使用,舉個例子:
- typedef struct
- {
- OVERLAPPED Overlapped;
- char Buffer[DATA_BUFSIZE];
- int BufferLen;
- int OperationType;
- }PER_IO_DATA
要想調用windowsAPI函數,同時爲其分配一個OVERLAPPED結構,只要簡單的撤銷對結構中OVERLAPPED機構的引用即可,如下所示:
- PER_IO_OPERATION_DATA PerIoData;
- WSABUF wbuf;
- DWORD Bytes,Flags;
- //初始化wbuf
- WSARecv(socket,&wbuf,1,&Bytes,&Flags,&(PerIoData.Overlapped),NULL);
在工作器線程的後面部分,GetQueuedCompletionStatus函數返回了一個重疊結構和完成鍵,獲取單I/O數據應使用宏CONTAINING_RECORD,例如:
- PER_IO_DATA *PerIoData = NULL;
- OVERLAPPED *lpOverlapped = NULL;
- ret = GetQueuedCompletionStatus(
- ComPortHandle,
- &Transferred,
- (PULONG_PTR)&CompletionKey,
- &lpOverlapped,
- INFINITE);
- //檢查成功的返回
- PerIoData = CONTAINING_RECORD(lpOverlapped,PER_IO_DATA,Overlapped);
應該使用這個宏;否則,結構PER_IO_DATA的成員OVERLAPPED就始終不得不首先出現,這會成爲一個危險的假設(多個開發者開發同一段代碼時尤爲嚴重)。
可以使用單I/O結構的一個字段來表示被投遞的操作類型,從而可以確定到底是哪個操作投遞到了句柄上。在我們的例子中,OpdrationType字段應設爲可以指示讀寫等操作的值。對單I/O操作數據來說,它最大的優點便是允許我們在同一個句柄上,同時管理多個I/O操作(讀寫,多個讀寫操作等等)。
Windows完成端口的一個重要方面是,所有重疊操作可確保按照應用程序安排好的順序執行。然而,不能確保從完成端口返回的完成通知也按上述順序執行。
設計一個工作器線程,令其使用單句柄數據和單I/O操作數據爲I/O請求提供服務:
- DWORD WINAPI ServerWorkerThread(LPVOID lpParam)
- {
- HANDLE CompletionPort = (HANDLE)lpParam;
- DWORD BytesTransferred;
- LPOVERLAPPED Overlapped;
- LPPER_HANDLE_DATA PerHandleData;
- LPPER_IO_DATA PerIoData;
- DWORD SendBytes, RecvBytes;
- DWORD Flags;
- while(TRUE)
- {
- //等待和完成端口關聯的任意套接字上的I/O完成
- ret = GetQueuedCompletionStauts(CompletionPort,
- &BytesTransferred,
- (LPWORD)&PerHandleData,
- (LPOVERLAPPED*)&PerIoData,
- INFINITE);
- //先檢查一下,看是否在套接字上發生錯誤;
- //如果發生了,關閉套接字,並清除和這個套接字關聯的單句柄數據和單I/O操作數據
- if(BytesTransferred==0 &&
- (PerIoData->OperationType == RECV_POSTED || PerIoData->OperationType == SEND_POSTED))
- {
- //BytesTransferred爲0時,表明套接字已被通信對方關閉,因此我們也要關閉套接字
- //注意:單句柄數據用來引用和I/O關聯的套接字
- closesocket(PerHandleData->Socket);
- GlobalFree(PerHandleData);
- GlobalFree(PerIoData);
- continue;
- }
- //爲完成的I/O請求提供服務。可以通過查看單I/O操作數據中包含的 OperationType字段,
- //來確定剛完成的是哪個I/O請求
- if(PerIoData->OperationType == RECV_POSTED)
- {
- //對PerIoData->Buffer中接收到的數據施加某種操作
- }
- //投遞另外一個WSASend或WSARecv操作
- //這裏只投遞一個WSARecv操作
- Flags = 0;
- //爲下一個重疊調用建立單I/O操作數據
- ZeroMemory(&(PerIoData->Overlapped),sizeof(OVERLAPPED));
- PerIoData->DataBuf.len = DATA_BUFSIZE;
- PerIoData->DataBuf.buf = PerIoData->Buffer;
- PerIoData->OperationType = RECV_POSTED;
- WSARecv(PerHandleData->Socket,
- &(PerIoData->DataBuf),
- 1,
- &RecvBytes,
- &Flags,
- &(PerIoData->Overlapped),
- NULL);
- }
- }
對於一個給定的重疊操作,如果發生錯誤,則GetQueuedCompletionStatus將返回FALSE,因爲完成端口是Windows採用的一種I/O構造機制,所有,如果調用GetLastError或WSAGetLastError,則錯誤代碼及可能是一個Windows錯誤代碼,而非Winsock錯誤。要想得到winsock錯誤代碼,可以在指定了套接字句柄和結構WSAOVERLAPPED的情況下,對已完成的操作調用WSAGetOverlappedResult,之後WSAGetLastError將返回轉換後的Winsock錯誤代碼。
最後要注意一處細節,是如何正確關閉I/O完成端口--特別是同時運行一個或多個線程,在幾個不同的套接字上執行I/O操作時。要注意的一個主要問題是,在進行重疊I/O操作時,應避免強行釋放OVERLAPPED結構。要想不出現這種情況,最好的辦法是針對每個套接字句柄,調用closesocket函數,則任何尚未進行的重疊I/O操作都會完成。一旦所有套接字句柄都已關閉,便須在完成端口上終止所有工作器線程的運行。要想做到這一點可以使用PostQueuedCompletionStatus函數,向每個工作器線程都發送一個特殊的完成數據包。該函數會提示每個線程立即結束並推出:
- BOOL PostQueuedCompletionStatus(
- HANDLE CompletionPort,
- DWORD dwNumberOfBytesTransferred,
- ULONG_PTR dwCompletionKey,
- LPOVERLAPPED lpOverlapped
- );
CompletionPort參數指明程序想向其發送一個完成數據包的完成端口對象。而就dwNumberOfBytesTransferred,dwCompletionKey,lpOverlapped這3個參數來說,每一個都允許指定一個值,直接傳遞給GetQueuedCompletionStatus函數中對應的參數,這樣,根據參數,決定何時退出。
=========================================================================
- #include<stdio.h>
- #include<winsow2.h>
- #pragma comment(lib, "ws2_32.lib")
- #define PORT 5050
- #define MSGSIE 1024
- typedef enum
- {
- RECV_POSTED
- }OPERATION_TYPE;
- typedef struct
- {
- OVERLAPPED overlap;
- WSABUF Buffer;
- char szMessage[MSGSIZE];
- DWORD NumberOfBytesRecvd;
- DWORD Flags;
- OPERATION_TYPE OpetationType;
- }PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
- DWORD WINAPI WorkerThread(LPVOID lpParam);
- int main()
- {
- WSADATA wsaData;
- SOCKET sListen, sClient;
- SOCKADDR_IN local, client;
- DWORD i, dwThreadId;
- int iAddrSize = sizeof(SOCKADDR_IN);
- HANDLE CompletionPort = INVALID_HANDLE_VALUE;
- SYSTEM_INFO sysinfo;
- LPPER_IO_OPERATION_DATA lpPerIoData = NULL;
- WSAStartup(MAKEWORD(2,2), &wsaData);
- CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
- GetSystemInfo(&sysinfo);
- for(i = 0; i<sysinfo.dwNumberOfProcessors; i++)
- {
- CreateThread(NULL,0,WorkerThread,CompletionPort,0,&dwThreadId);
- }
- sListen = socket(AF_INET,SOCK_STREAM,0);
- memset(&local,0,sizeof(SOCKADDR_IN));
- local.sin_family = AF_INET;
- local.sin_port = htons(PORT);
- local.sin_addr.s_addr = htonl(INADDR_ANY);
- bind(sListen,(SOCKADDR*)&local,sizeof(SOCKADDR_IN));
- listen(sListen, 5);
- while(TRUE)
- {
- sCient = accept(sListen,(SOCKADDR*)&client,&iAddrSize);
- printf("Accept Client:%s:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
- CreateIoCompletionPort((HANDLE)sClient,CompletionPort,(DWORD)sClient,0);
- lpPetIoData=(LPPER_IO_OPERATION_DATA)HeapAlloc(
- GetProcessHeap(),
- HEAP_ZERO_MEMORY,
- sizeof(PER_IO_OPERATION_DATA));
- lpPerIoData->Buffer.len = MSGSIZE;
- lpPerIoData->Buffer.buf = lpPerIoData->szMessage;
- lpPerIoData->OpetationType = RECV_POSTED;
- WSARecv(sClient,
- &lpPerIoData->Buffer,
- 1,
- &lpPerIoData->NumberOfBytesRecvd,
- &lpPerIoData->Flags,
- &lpPerIoData->overlap,
- NULL);
- }
- PostQueuedCompletionStauts(CompletionPort,0xFFFFFFFF,0,NULL);
- CloseHandle(CompletionPort);
- closesocket(sListen);
- WSACleanup();
- return 0;
- }
- DWORD WINAPI WorkerThread(LPVOID lpParam)
- {
- HANDLE CompletionPort = (HANDLE)lpParam;
- DWORD dwBytesTransferred;
- SOCKET sClient;
- LPPER_IO_OPERATION_DATA lpPerIoData = NULL;
- while(TRUE)
- {
- GetQueuedCompletionStatus(CompletionPort,
- &dwBytesTransferred,
- (DWORD*)sClient,
- (LPOVERLAPPED*)&lpPerIoData,
- INFINITE);
- if(dwBytesTransferred==0xFFFFFFFF)
- {
- return 0;
- }
- if(lpPerIoData->OpetationType==RECV_POSTED)
- {
- if(dwBytesTransferred==0)
- {
- 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->OpetationType = RECV_POSTED;
- WSARecv(sClient,
- &lpPerIoData->Buffer,
- 1,
- &lpPerIoData->NumberOfBytesRecvd,
- &lpPerIoData->Flags,
- &lpPerIoData->overlap,
- NULL);
- }
- }
- }
- return 0;
- }
服務器端得主要流程:
1.創建完成端口對象
2.創建工作者線程(這裏工作者線程的數量是按照CPU的個數來決定的,這樣可以達到最佳性能)
3.創建監聽套接字,綁定,監聽,然後程序進入循環
4.在循環中,我做了以下幾件事情:
(1).接受一個客戶端連接
(2).將該客戶端套接字與完成端口綁定到一起(還是調用CreateIoCompletionPort,但這次的作用不同),注意,按道理來講,此時傳遞給CreateIoCompletionPort的第三個參數應該是一個完成鍵,一般來講,程序都是傳遞一個單句柄數據結構的地址,該單句柄數據包含了和該客戶端連接有關的信息,由於我們只關心套接字句柄,所以直接將套接字句柄作爲完成鍵傳遞;
(3).觸發一個WSARecv異步調用,這次又用到了“尾隨數據”,使接收數據所用的緩衝區緊跟在WSAOVERLAPPED對象之後,此外,還有操作類型等重要信息。
在工作者線程的循環中,我們
1.調用GetQueuedCompletionStatus取得本次I/O的相關信息(例如套接字句柄、傳送的字節數、單I/O數據結構的地址等等)
2.通過單I/O數據結構找到接收數據緩衝區,然後將數據原封不動的發送到客戶端
3.再次觸發一個WSARecv異步操作