SOCKET編程進階之完成端口
原文地址:http://blog.csdn.net/echoff/archive/2007/09/23/1797326.aspx
一、什麼是完成端口?
完成端口---是一種WINDOWS內核對象。完成端口用於異步方式的重疊I/0情況下,當然重疊I/O不一定非使用完成端口不可,還有設備內核對象、事件對象、告警I/0等。但是完成端口內部提供了線程池的管理,可以避免反覆創建線程的開銷,同時可以根據CPU的個數靈活的決定線程個數,而且可以讓減少線程調度的次數從而提高性能。
二、完成端口的內部機制
1)創建完成端口
完成端口是一個內核對象,使用時他總是要和至少一個有效的設備句柄進行關聯,完成端口是一個複雜的內核對象,創建它的函數是:
HANDLE CreateIoCompletionPort(
IN HANDLE FileHandle,
IN HANDLE ExistingCompletionPort,
IN ULONG_PTR CompletionKey,
IN DWORD NumberOfConcurrentThreads
);
通常創建工作分兩步:
第一步,創建一個新的完成端口內核對象,可以使用下面的函數:
HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
{
return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
};
第二步,將剛創建的完成端口和一個有效的設備句柄關聯起來,可以使用下面的函數:
bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
{
HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
return h==hCompPort;
};
說明
a)CreateIoCompletionPort函數也可以一次性的既創建完成端口對象,又關聯到一個有效的設備句柄
b)CompletionKey是一個可以自己定義的參數,我們可以把一個結構的地址賦給它,然後在合適的時候取出來使用,最好要保證結構裏面的內存不是分配在棧上,除非你有十分的把握內存會保留到你要使用的那一刻。
c)NumberOfConcurrentThreads通常用來指定要允許同時運行的的線程的最大個數。通常我們指定爲0,這樣系統會根據CPU的個數來自動確定。
創建和關聯的動作完成後,系統會將完成端口關聯的設備句柄、完成鍵作爲一條紀錄加入到這個完成端口的設備列表中。如果你有多個完成端口,就會有多個對應的設備列表。如果設備句柄被關閉,則表中自動刪除該紀錄。
2)完成端口線程的工作原理
完成端口可以幫助我們管理線程池,但是線程池中的線程需要我們使用_beginthreadex來創建,憑什麼通知完成端口管理我們的新線程呢?答案在函數GetQueuedCompletionStatus。該函數原型:
BOOL GetQueuedCompletionStatus(
IN HANDLE CompletionPort,
OUT LPDWORD lpNumberOfBytesTransferred,
OUT PULONG_PTR lpCompletionKey,
OUT LPOVERLAPPED *lpOverlapped,
IN DWORD dwMilliseconds
);
這個函數試圖從指定的完成端口的I/0完成隊列中抽取紀錄。只有當重疊I/O動作完成的時候,完成隊列中才有紀錄。凡是調用這個函數的線程將被放入到完成端口的等待線程隊列中,因此完成端口就可以在自己的線程池中幫助我們維護這個線程。
完成端口的I/0完成隊列中存放了當重疊I/0完成的結果---- 一條紀錄,該紀錄擁有四個字段,前三項就對應GetQueuedCompletionStatus函數的2、3、4參數,最後一個字段是錯誤信息dwError。我們也可以通過調用PostQueudCompletionStatus模擬完成了一個重疊I/0操作。
當I/0完成隊列中出現了紀錄,完成端口將會檢查等待線程隊列,該隊列中的線程都是通過調用GetQueuedCompletionStatus函數使自己加入隊列的。等待線程隊列很簡單,只是保存了這些線程的ID。完成端口會按照後進先出的原則將一個線程隊列的ID放入到釋放線程列表中,同時該線程將從等待GetQueuedCompletionStatus函數返回的睡眠狀態中變爲可調度狀態等待CPU的調度。
基本上情況就是如此,所以我們的線程要想成爲完成端口管理的線程,就必須要調用
GetQueuedCompletionStatus函數。出於性能的優化,實際上完成端口還維護了一個暫停線程列表,具體細節可以參考《Windows高級編程指南》,我們現在知道的知識,已經足夠了。
3)線程間數據傳遞
線程間傳遞數據最常用的辦法是在_beginthreadex函數中將參數傳遞給線程函數,或者使用全局變量。但是完成端口還有自己的傳遞數據的方法,答案就在於CompletionKey和OVERLAPPED參數。
CompletionKey被保存在完成端口的設備表中,是和設備句柄一一對應的,我們可以將與設備句柄相關的數據保存到CompletionKey中,或者將CompletionKey表示爲結構指針,這樣就可以傳遞更加豐富的內容。這些內容只能在一開始關聯完成端口和設備句柄的時候做,因此不能在以後動態改變。
OVERLAPPED參數是在每次調用ReadFile這樣的支持重疊I/0的函數時傳遞給完成端口的。我們可以看到,如果我們不是對文件設備做操作,該結構的成員變量就對我們幾乎毫無作用。我們需要附加信息,可以創建自己的結構,然後將OVERLAPPED結構變量作爲我們結構變量的第一個成員,然後傳遞第一個成員變量的地址給ReadFile函數。因爲類型匹配,當然可以通過編譯。當GetQueuedCompletionStatus函數返回時,我們可以獲取到第一個成員變量的地址,然後一個簡單的強制轉換,我們就可以把它當作完整的自定義結構的指針使用,這樣就可以傳遞很多附加的數據了。太好了!只有一點要注意,如果跨線程傳遞,請注意將數據分配到堆上,並且接收端應該將數據用完後釋放。我們通常需要將ReadFile這樣的異步函數的所需要的緩衝區放到我們自定義的結構中,這樣當GetQueuedCompletionStatus被返回時,我們的自定義結構的緩衝區變量中就存放了I/0操作的數據。
CompletionKey和OVERLAPPED參數,都可以通過GetQueuedCompletionStatus函數獲得。
4)線程的安全退出
很多線程爲了不止一次的執行異步數據處理,需要使用如下語句
while (true)
{
.。。。。。。
GetQueuedCompletionStatus(...);
。。。。。。
}
那麼如何退出呢,答案就在於上面曾提到的PostQueudCompletionStatus函數,我們可以用它發送一個自定義的包含了OVERLAPPED成員變量的結構地址,裏面包含一個狀態變量,當狀態變量爲退出標誌時,線程就執行清除動作然後退出。
//接上文
寫了一下午,終於寫完了這個“完成端口”。
到今天爲止,寫完了Overlapped I/O Event、Overlapped I/O completion Routine和completion Port。一路寫過來的確學到了不少東西,也清楚地看到到微軟在遇到問題並解決問題的方法;不得不承認,微軟~還是很強的。呵呵~
這也讓我明白一件事:遇到困難,不要望而卻步;只要你勇於探索,一切都將是那麼簡單。(聽起來有點自戀的感覺^_^)
“完成端口”模型是迄今爲止最爲複雜的一種I/O模型。然而,假若一個應用程序同時需要管理爲數衆多的套接字,那麼採用這種模型,往往可以達到最佳的系統性能!但不幸的是,該模型只適用於Windows NT和Windows 2000操作系統。因其設計的複雜性,只有在你的應用程序需要同時管理數百乃至上千個套接字的時候,而且希望隨着系統內安裝的CPU數量的增多,應用程序的性能也可以線性提升,才應考慮採用“完成端口”模型。要記住的一個基本準則是,假如要爲Windows NT或Windows 2000開發高性能的服務器應用,同時希望爲大量套接字I/O請求提供服務(Web服務器便是這方面的典型例子),那麼I/O完成端口模型便是最佳選擇!
我們基本上按下述步驟行事:
1) 創建一個完成端口。第四個參數保持爲0,指定在完成端口上,每個處理器一次只允許執行一個工作者線程。
2) 判斷系統內到底安裝了多少個處理器。
3) 創建工作者線程,根據步驟2)得到的處理器信息,在完成端口上,爲已完成的I/O請求提供服務。在這個簡單的例子中,我們爲每個處理器都只創建一個工作者線程。這是由於事先已預計到,到時不會有任何線程進入“掛起”狀態,造成由於線程數量的不足,而使處理器空閒的局面(沒有足夠的線程可供執行)。調用CreateThread函數時,必須同時提供一個工作者例程,由線程在創建好執行。本節稍後還會詳細討論線程的職責。
4) 準備好一個監聽套接字,在端口1234上監聽進入的連接請求。
5) 使用accept函數,接受進入的連接請求。
6) 創建一個數據結構,同時在結構中存入接受的套接字句柄。
7) 調用CreateIoCompletionPort,將自accept返回的新套接字句柄同完成端口關聯到一起。通過完成鍵(CompletionKey)參數,將單句柄數據結構傳遞給CreateIoCompletionPort。
8) 開始在已接受的連接上進行I/O操作。在此,我們希望通過重疊I/O機制,在新建的套接字上投遞一個或多個異步WSARecv或WSASend請求。這些I/O請求完成後,一個工作者線程會爲I/O請求提供服務,同時繼續處理未來的I/O請求,稍後便會在步驟3)指定的工作者例程
中,體驗到這一點。
9) 重複步驟5) ~ 8),直至服務器中止。
- #pragma comment(lib,"ws2_32.lib")
- #include <winsock2.h>
- #include <stdio.h>
- //////////////////////////////////////////////////////////////////////////
- //僅供測試軟件用
- #include "Protocol.h"
- #define DATA_BUFSIZE 1024 // 接收緩衝區大小
- typedef enum{ IOSEND,IORECV,IOQUIT } IO_TYPE;
- typedef struct _SOCKET_INFORMATION {
- OVERLAPPED Overlapped;
- SOCKET Socket;
- IO_TYPE IoType;
- char buffer[DATA_BUFSIZE];
- WSABUF DataBuf;
- DWORD BytesSEND;
- DWORD BytesRECV;
- } SOCKET_INFORMATION, * LPSOCKET_INFORMATION;
- DWORD Flags = 0,
- Bytes = 0;
- DWORD WINAPI WorkThread(LPVOID CompletionPortID);
- DWORD WINAPI AcceptThread(LPVOID lpParameter)
- {
- WSADATA wsaData;
- HANDLE hCompPort;
- DWORD ThreadID;
- DWORD Ret;
- if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
- {
- printf("WSAStartup failed with error %d/n", Ret);
- return FALSE;
- }
- if ((hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
- {
- printf( "CreateIoCompletionPort failed with error: %d/n", GetLastError());
- return FALSE;
- }
- //根據CPU個數來創建線程,以達到最佳性能
- SYSTEM_INFO SystemInfo;
- GetSystemInfo(&SystemInfo);
- for(unsigned int i=0; i<SystemInfo.dwNumberOfProcessors*2; i++)
- {
- HANDLE ThreadHandle;
- if ((ThreadHandle = CreateThread(NULL, 0, WorkThread, hCompPort, 0, &ThreadID)) == NULL)
- {
- printf("CreateThread() failed with error %d/n", GetLastError());
- return FALSE;
- }
- CloseHandle(ThreadHandle);
- }
- SOCKET ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, NULL, WSA_FLAG_OVERLAPPED);
- SOCKADDR_IN ServerAddr;
- ServerAddr.sin_family = AF_INET;
- ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
- ServerAddr.sin_port = htons(1234);
- bind(ListenSocket,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr));
- listen(ListenSocket,100);
- printf("listenning.../n");
- SOCKADDR_IN ClientAddr;
- int addr_length=sizeof(ClientAddr);
- while (TRUE)
- {
- LPSOCKET_INFORMATION SI = new SOCKET_INFORMATION;
- if ((SI->Socket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET)
- {
- printf("accept ip:%s port:%d/n",inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port);
- //相關參數初始化
- memset(&SI->Overlapped,0,sizeof(WSAOVERLAPPED));
- memset(SI->buffer, 0, DATA_BUFSIZE);
- SI->DataBuf.buf = SI->buffer;
- SI->DataBuf.len = DATA_BUFSIZE;
- SI->BytesRECV = 0;
- SI->BytesSEND = 0;
- SI->IoType = IORECV;
- //////////////////////////////////////////////////////////////////////////
- //僅供測試軟件用
- HeaderMessage recvMsg;
- if (recv(SI->Socket, (char*)&recvMsg, sizeof(recvMsg), 0) <= 0)
- {
- printf("初始參數交互失敗");
- }
- if (CreateIoCompletionPort((HANDLE)SI->Socket, hCompPort, (DWORD)SI, 0) == NULL)
- {
- printf("CreateIoCompletionPort failed with error %d/n", GetLastError());
- return FALSE;
- }
- //發出一個重疊I/O請求
- if(WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
- {
- if(WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("disconnect/n");
- closesocket(SI->Socket);
- delete SI;
- continue;
- }
- }
- }
- }
- return FALSE;
- }
- DWORD WINAPI WorkThread(LPVOID CompletionPortID)
- {
- HANDLE hCompPort = (HANDLE)CompletionPortID;
- while (TRUE)
- {
- DWORD BytesTransferred = 0;
- LPSOCKET_INFORMATION SI = NULL;
- LPWSAOVERLAPPED Overlapped = NULL;
- //線程進入線程池,等待被喚醒
- if (GetQueuedCompletionStatus(hCompPort, &BytesTransferred, (LPDWORD)&SI, &Overlapped, INFINITE))
- {
- if (0 == BytesTransferred && IOQUIT != SI->IoType)
- {
- printf("disconnect/n");
- closesocket(SI->Socket);
- delete SI;
- continue;
- }
- switch(SI->IoType)
- {
- case IORECV:
- {
- //目前的功能是將接收到的數據原封不動的返回
- SI->DataBuf.len = BytesTransferred;
- SI->BytesRECV = BytesTransferred;
- SI->IoType = IOSEND;
- if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
- {
- if(WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("disconnect/n");
- closesocket(SI->Socket);
- delete SI;
- continue;
- }
- }
- break;
- }
- case IOSEND:
- {
- SI->BytesSEND += BytesTransferred;
- //返回是否徹底,若未發完,接着發
- if (SI->BytesSEND < SI->BytesRECV)
- {
- SI->DataBuf.buf += BytesTransferred;
- SI->DataBuf.len -= BytesTransferred;
- SI->IoType = IOSEND;
- if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
- {
- if(WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("disconnect/n");
- closesocket(SI->Socket);
- delete SI;
- continue;
- }
- }
- }
- else if (SI->BytesSEND > SI->BytesRECV)
- {
- printf("BytesSEND:%d > BytesRECV:%d/n",SI->BytesSEND,SI->BytesRECV);
- memset(SI->buffer, 0, DATA_BUFSIZE);
- SI->BytesRECV = 0;
- SI->BytesSEND = 0;
- SI->IoType = IORECV;
- SI->DataBuf.len = DATA_BUFSIZE;
- SI->DataBuf.buf = SI->buffer;
- }
- else
- {
- memset(SI->buffer, 0, DATA_BUFSIZE);
- SI->BytesRECV = 0;
- SI->BytesSEND = 0;
- SI->IoType = IORECV;
- SI->DataBuf.len = DATA_BUFSIZE;
- SI->DataBuf.buf = SI->buffer;
- if (WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
- {
- if(WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("disconnect/n");
- closesocket(SI->Socket);
- delete SI;
- continue;
- }
- }
- }
- break;
- }
- case IOQUIT:
- {
- //讓線程安全退出
- return FALSE;
- break;
- }
- default:
- break;
- }
- }
- }
- return FALSE;
- }
- void main()
- {
- HANDLE hThreads = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL);
- WaitForSingleObject(hThreads,INFINITE);
- printf("exit/n");
- CloseHandle(hThreads);
- }