SOCKET編程進階之完成端口

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),直至服務器中止。
  1. #pragma comment(lib,"ws2_32.lib")
  2. #include <winsock2.h>
  3. #include <stdio.h>
  4. //////////////////////////////////////////////////////////////////////////
  5. //僅供測試軟件用
  6. #include "Protocol.h"
  7.  
  8. #define DATA_BUFSIZE 1024        // 接收緩衝區大小
  9. typedef enum{ IOSEND,IORECV,IOQUIT } IO_TYPE;
  10. typedef struct _SOCKET_INFORMATION {
  11.         OVERLAPPED Overlapped;
  12.         SOCKET        Socket;
  13.         IO_TYPE  IoType;
  14.         char                buffer[DATA_BUFSIZE];
  15.         WSABUF        DataBuf;
  16.         DWORD        BytesSEND;
  17.         DWORD        BytesRECV;
  18. } SOCKET_INFORMATION, * LPSOCKET_INFORMATION;
  19. DWORD   Flags = 0,
  20.                 Bytes = 0;
  21. DWORD WINAPI WorkThread(LPVOID CompletionPortID);
  22. DWORD WINAPI AcceptThread(LPVOID lpParameter)
  23. {
  24.         WSADATA wsaData;
  25.         HANDLE hCompPort;
  26.         DWORD ThreadID;
  27.         DWORD Ret;
  28.         if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
  29.         {
  30.                 printf("WSAStartup failed with error %d/n", Ret);
  31.                 return FALSE;
  32.         }
  33.         if ((hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
  34.         {
  35.                 printf( "CreateIoCompletionPort failed with error: %d/n", GetLastError());
  36.                 return FALSE;
  37.         }
  38.         //根據CPU個數來創建線程,以達到最佳性能
  39.         SYSTEM_INFO SystemInfo;
  40.         GetSystemInfo(&SystemInfo);
  41.         for(unsigned int i=0; i<SystemInfo.dwNumberOfProcessors*2; i++)
  42.         {
  43.                 HANDLE ThreadHandle;
  44.                 if ((ThreadHandle = CreateThread(NULL, 0, WorkThread, hCompPort, 0, &ThreadID)) == NULL)
  45.                 {
  46.                         printf("CreateThread() failed with error %d/n", GetLastError());
  47.                         return FALSE;
  48.                 }
  49.                 CloseHandle(ThreadHandle);
  50.         }
  51.         SOCKET ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, NULL, WSA_FLAG_OVERLAPPED);
  52.         SOCKADDR_IN ServerAddr;
  53.         ServerAddr.sin_family = AF_INET;
  54.         ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
  55.         ServerAddr.sin_port = htons(1234);
  56.         bind(ListenSocket,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr));
  57.         listen(ListenSocket,100);
  58.         printf("listenning.../n");
  59.         SOCKADDR_IN ClientAddr;
  60.         int addr_length=sizeof(ClientAddr);
  61.         while (TRUE)
  62.         {
  63.                 LPSOCKET_INFORMATION  SI = new SOCKET_INFORMATION;
  64.                 if ((SI->Socket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET)
  65.                 {
  66.                         printf("accept ip:%s port:%d/n",inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port);
  67.                         //相關參數初始化
  68.                         memset(&SI->Overlapped,0,sizeof(WSAOVERLAPPED));
  69.                         memset(SI->buffer, 0, DATA_BUFSIZE);
  70.                         SI->DataBuf.buf = SI->buffer;
  71.                         SI->DataBuf.len = DATA_BUFSIZE;
  72.                         SI->BytesRECV        = 0;
  73.                         SI->BytesSEND        = 0;
  74.                         SI->IoType                = IORECV;
  75.                         //////////////////////////////////////////////////////////////////////////
  76.                         //僅供測試軟件用
  77.                         HeaderMessage recvMsg;
  78.                         if (recv(SI->Socket, (char*)&recvMsg, sizeof(recvMsg), 0) <= 0) 
  79.                         {
  80.                                 printf("初始參數交互失敗");
  81.                         }
  82.                         if (CreateIoCompletionPort((HANDLE)SI->Socket, hCompPort, (DWORD)SI, 0) == NULL)
  83.                         {
  84.                                 printf("CreateIoCompletionPort failed with error %d/n", GetLastError());
  85.                                 return FALSE;
  86.                         }
  87.                         //發出一個重疊I/O請求
  88.                         if(WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  89.                         {
  90.                                 if(WSAGetLastError() != WSA_IO_PENDING)
  91.                                 {
  92.                                         printf("disconnect/n");
  93.                                         closesocket(SI->Socket); 
  94.                                         delete SI;
  95.                                         continue;
  96.                                 }
  97.                         }
  98.                 }
  99.                 
  100.         }
  101. return FALSE;
  102. }
  103. DWORD WINAPI WorkThread(LPVOID CompletionPortID)
  104. {
  105.         HANDLE hCompPort = (HANDLE)CompletionPortID;
  106.         while (TRUE)
  107.         {
  108.                 DWORD BytesTransferred = 0;
  109.                 LPSOCKET_INFORMATION SI = NULL;
  110.                 LPWSAOVERLAPPED Overlapped = NULL;
  111.                 //線程進入線程池,等待被喚醒
  112.                 if (GetQueuedCompletionStatus(hCompPort, &BytesTransferred, (LPDWORD)&SI, &Overlapped, INFINITE))
  113.                 {
  114.                         if (0 == BytesTransferred && IOQUIT != SI->IoType)
  115.                         {
  116.                                 printf("disconnect/n");
  117.                                 closesocket(SI->Socket); 
  118.                                 delete SI;
  119.                                 continue;
  120.                         }
  121.                         switch(SI->IoType)
  122.                         {
  123.                         case IORECV:
  124.                                 {
  125.                                         //目前的功能是將接收到的數據原封不動的返回
  126.                                         SI->DataBuf.len = BytesTransferred;
  127.                                         SI->BytesRECV = BytesTransferred;
  128.                                         SI->IoType = IOSEND;
  129.                                         if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  130.                                         {
  131.                                                 if(WSAGetLastError() != WSA_IO_PENDING)
  132.                                                 {
  133.                                                         printf("disconnect/n");
  134.                                                         closesocket(SI->Socket); 
  135.                                                         delete SI;
  136.                                                         continue;
  137.                                                 }
  138.                                         }
  139.                                 break;
  140.                                 }
  141.                         case IOSEND:
  142.                                 {
  143.                                         SI->BytesSEND += BytesTransferred;
  144.                                         //返回是否徹底,若未發完,接着發
  145.                                         if (SI->BytesSEND < SI->BytesRECV)
  146.                                         {
  147.                                                 SI->DataBuf.buf += BytesTransferred; 
  148.                                                 SI->DataBuf.len -= BytesTransferred; 
  149.                                                 SI->IoType = IOSEND;
  150.                                                 if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  151.                                                 {
  152.                                                         if(WSAGetLastError() != WSA_IO_PENDING)
  153.                                                         {
  154.                                                                 printf("disconnect/n");
  155.                                                                 closesocket(SI->Socket); 
  156.                                                                 delete SI;
  157.                                                                 continue;
  158.                                                         }
  159.                                                 }
  160.                                         }
  161.                                         else if (SI->BytesSEND > SI->BytesRECV)
  162.                                         {
  163.                                                 printf("BytesSEND:%d > BytesRECV:%d/n",SI->BytesSEND,SI->BytesRECV);
  164.                                                 memset(SI->buffer, 0, DATA_BUFSIZE);
  165.                                                 SI->BytesRECV = 0;
  166.                                                 SI->BytesSEND = 0;
  167.                                                 SI->IoType = IORECV;
  168.                                                 SI->DataBuf.len = DATA_BUFSIZE;
  169.                                                 SI->DataBuf.buf = SI->buffer;
  170.                                         }
  171.                                         else
  172.                                         {
  173.                                                 memset(SI->buffer, 0, DATA_BUFSIZE);
  174.                                                 SI->BytesRECV = 0;
  175.                                                 SI->BytesSEND = 0;
  176.                                                 SI->IoType = IORECV;
  177.                                                 SI->DataBuf.len = DATA_BUFSIZE;
  178.                                                 SI->DataBuf.buf = SI->buffer;
  179.                                                 if (WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
  180.                                                 {
  181.                                                         if(WSAGetLastError() != WSA_IO_PENDING)
  182.                                                         {
  183.                                                                 printf("disconnect/n");
  184.                                                                 closesocket(SI->Socket); 
  185.                                                                 delete SI;
  186.                                                                 continue;
  187.                                                         }
  188.                                                 }
  189.                                         }
  190.                                 break;
  191.                                 }
  192.                         case IOQUIT:
  193.                                 {
  194.                                         //讓線程安全退出
  195.                                         return FALSE;
  196.                                 break;
  197.                                 }
  198.                                 
  199.                         default:
  200.                                 break;
  201.                         }
  202.                 }        
  203.         }        
  204. return FALSE;
  205. }
  206. void main()   
  207. {
  208.         HANDLE hThreads = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL); 
  209.         
  210.         WaitForSingleObject(hThreads,INFINITE);
  211.         printf("exit/n");
  212.         CloseHandle(hThreads);
  213.  
  214.  


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章