IOCP小結

當應用程序必須一次管理多個套接字時,完成端口模型提供了最好的系統性能。這個模型也提供了最好的伸縮性,它非常適合用來處理上百上千個套接字。

IOCP模型是事先開好了N個線程,存儲在線程池中,讓他們hold。然後將所有用戶的請求都投遞到一個完成端口上,然後N個工作線程逐一地從完成端口中取得用戶消息並加以處理。這樣就避免了爲每個用戶開一個線程。既減少了線程資源,又提高了線程的利用率。

一 什麼是完成端口(completion port)對象

I/O完成端口是應用程序使用線程池處理異步I/O請求的一種機制。處理多個併發異步I/O請求時,使用I/O完成端口比在I/O請求時創建線程更快更有效。

二 使用IOCP的方法

創建完成端口對象

使用完成端口模型,首先要調用CreateIoCompletionPort函數創建一個完成端口對象,Winsock將使用這個對象爲任意數量的套接字句柄管理I/O請求。函數定義如下:

HANDLE CreateIoCompletionPort(  
  HANDLE hFile,                          //要關聯的套接字句柄
  HANDLE hExistingCompletionPort,         //創建的完成端口對象句柄
  ULONG_PTR CompletionKey,        //制定一個句柄唯一數據,它將與套接字句柄關聯在一起。應用程序可以在此存儲任意類型的信息,通常是一個指針
  DWORD dwNumberOfConcurrentThreads   //允許在完成端口上同時執行的線程的數量
  );  

此函數有兩個不同的功能:
1 創建一個完成端口對象;
2 將一個或多個文件句柄(套接字句柄)關聯到I/O完成端口對象

I/O服務線程和完成端口

成功創建完成端口對象之後,就可以向這個完成端口對象關聯套接字句柄了。在關聯套接字之前,需要先創建一個或多個工作線程(成爲I/O服務線程),在完成端口上執行並處理投遞到完成端口上的I/O請求。

有了足夠的工作線程來處理完成端口上的I/O請求後,就該爲完成端口關聯套接字句柄,這使用到了CreateIoCompletionPort的前三個參數。CompletionKey參數通常用來描述與套接字相關的信息,所以稱它爲句柄唯一(per=handle)數據。

完成端口和重疊I/O

在完成端口關聯套接字句柄之後,便可以通過在套接字上投遞重疊發送和接收請求處理I/O了。這些I/O操作完成時,I/O系統會向完成端口對象發送一個完成通知封包。

I/O完成端口以先進先出的方式爲這些封包排隊。應用程序使用GetQueuedCompletionSatus函數可以取得這些隊列中的封包。這個函數應該在處理完成端口對象I/O的服務線程中調用。

GetQueuedCompletionStatus(
       HANDLECompletionPort,            //完成端口對象句柄
       LPDWORDlpNumberOfBytes      	    //取得I/O操作期間傳輸的字節數
       PULONG_PTRlpCompletionKey, 	    //取得在關聯套接字時指定的句柄唯一數據(指針)
       LPOVERLAPPED*lpOverlapped,      //取得投遞I/O操作時指定的OVERLAPPED 結構
       DWORD dwMilliseconds            //如果完成端口沒有完成封包,此參數指定了等待的事件,INFINIE爲無窮大
       )

I/O服務線程調用GetQueuedCompletionStatus函數取得有事件發生的套接字的信息,通過lpNumberOfBytes參數得到傳輸的字節數量,通過lpCompletionKey參數得到與套接字關聯的句柄唯一(per-handle)數據,通過lpOverlapped參數得到投遞I/O請求時使用的重疊對象地址,進一步得到I/O唯一(per-I/O)數據。

lpCompletionKey參數包含了我們稱爲per-Handle的數據,因爲當套接字第一次與完成端口關聯時,這個數據就關聯到一個套接字句柄。這是傳遞給CreateIoCompletionPort函數的CompletionKey參數。

lpOverlapped參數指向一個OVERLAPPED結構,結構後面便是我們稱爲per-I/O的數據,這可以是工作線程處理完成封包時想要知道的任何信息。

三 恰當地關閉IOCP

四 IOCP大概的處理流程

1 創建一個完成端口
2 創建一個線程A
3 A線程循環調用GetQueuedCompletionStatus()函數來得到IO操作結果
4 主線程循環裏調用accept等待客戶端連接
5 主線程裏accept返回新連接後,把這個新的套接字句柄用CreateIoCompletionPort()關聯到完成端口,然後發出一個異步WSASend或者WSARecv調用,因爲是異步函數,WSASend/WSARecv會馬上返回,實際的發送或者接收操作由Windows系統去做。
6 主線程繼續下一次循環,阻塞在accept這裏等待新的客戶連接
7 Windows系統完成WSASend或者WSARecv的操作,把結果發到完成端口。
8 A線程裏的GetQueuedCompletionStatus()馬上返回,並從完成端口取得剛完成的WSASend/WSARecv的結果。
9 在A線程裏對這些數據進行處理(如果處理過程很耗時,需要新開線程處理),然後緊接着發出WSASend/WSARecv,並繼續下一次循環阻塞在GetQueuedCompletionStatus()這裏。

流程圖:

在這裏插入圖片描述
上述流程中有兩種類型的線程 —主線程和它創建的線程,主線程創建監聽套接字,創建額外的工作線程,關聯IOCP,負責等待和接受到來的連接等;由主線程創建的線程負責處理I/O事件,這些線程調用GetQueuedCompletionStatus函數在完成端口對象上等待完成的I/O操作。

五 一個簡單示例具體編程流程

1 創建一個完成端口對象,創建一個或多個服務線程,服務線程調用GetQueuedCompletionStatus取得完成I/O通知信息。主線程接收連接,將新套接字關聯到完成端口上,然後在新連接上投遞Read I/O異步請求。

2 服務線程從GetQueuedCompletionStatus函數返回後,根據per-handle I/O數據中的信息,做出相應的處理,並繼續投遞下一個Read I/O異步請求。

#define _WIN32_WINNT 0x0400   
 
#include<windows.h>
#include<cstdio>
#include"InitSocket.h"
 
#define BUFFER_SIZE 2048
 
CInitSock initSock ; //進入main函數前已經進行了初始化
 
typedef	struct _PER_HANDLE_DATA			//per-handle數據
{
	SOCKET s ;							//對應的套接字句柄
	sockaddr_in addr ;					//客戶方地址
}  PER_HANDLE_DATA ,*PPER_HANDLE_DATA ;
 
 
/********************************************************/
//包含版本
typedef	struct _PER_IO_DATA		//per-I/O數據
{
	OVERLAPPED ol ;				//重疊結構,必須放作第一個結構,用C++派生的方法更好
	char buf[BUFFER_SIZE] ;		//數據緩衝區
	int nOperationType ;		//操作類型
 
#define	OP_READ 1				//操作類型碼
#define	OP_WRITE 2
#define	OP_ACCEPT 3
} PER_IO_DATA,*PPER_IO_DATA ;
/**********************************************************/
 
/***********************************************************
 * 派生版本的
class _PER_IO_DATA : public OVERLAPPED
{
	public :
		char buf[BUFFER_SIZE] ;
		int nOperationType ;
#define	OP_READ 1				//操作類型碼
#define	OP_WRITE 2
#define	OP_ACCEPT 3
} ;
typedef	 _PER_IO_DATA PER_IO_DATA ;
typedef	PER_IO_DATA *PPER_IO_DATA;
***************************************************/
 
 
DWORD WINAPI ServerThread(LPVOID lpParam)  ;
 
int main(void)
{
	int nPort = 4567 ;
 
	//創建完成端口對象,創建工作線程處理完成端口對象中的事件
	HANDLE hCompletion = CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0) ;
	CreateThread(NULL,0,ServerThread,(LPVOID)hCompletion,0,0) ;
 
	//創建監聽套接字,綁定到本地地址,開始監聽
	SOCKET sListen = socket(AF_INET,SOCK_STREAM,0) ;
	SOCKADDR_IN si ;
	si.sin_family = AF_INET ;
	si.sin_port = ntohs(nPort) ;
	si.sin_addr.s_addr = INADDR_ANY ;
 
	bind(sListen,(sockaddr*)&si,sizeof(si)) ;
	listen(sListen,5) ;
 
	//循環處理到來的連接
	while(TRUE)
	{
		//等待接受未決的連接請求
		SOCKADDR_IN saRemote ;
		int nRemoteLen = sizeof(saRemote) ;
		SOCKET sNew = accept(sListen,(sockaddr*)&saRemote,&nRemoteLen) ;
 
		//接受到新連接之後,爲創建一個per-handle數據,並將它們關聯到完成端口對象
		PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)GlobalAlloc(GPTR,sizeof(PER_HANDLE_DATA)) ;
		pPerHandle->s = sNew ;
 
		memcpy(&pPerHandle->addr,&saRemote,nRemoteLen) ;
		CreateIoCompletionPort((HANDLE)pPerHandle->s,hCompletion,(DWORD)pPerHandle,0) ; //關聯,不是創建.涉及到轉型,將指針轉爲DWORD,與pPerHandle->s關聯的唯一數據就是PerHandle
 
		//投遞一個接收請求
		PPER_IO_DATA pPerIO = (PPER_IO_DATA)GlobalAlloc(GPTR,sizeof(PER_IO_DATA)) ;
		pPerIO->nOperationType = OP_READ ;
		WSABUF buf ;
		buf.buf = pPerIO->buf ;
		buf.len = BUFFER_SIZE ;
		DWORD dwRecv ;
		DWORD dwFlags = 0 ;
		//作爲引子的接收請求,引發接收操作
		//與該套接字相關的OVERLAPPED結構
		WSARecv(pPerHandle->s,&buf,1,&dwRecv,&dwFlags,&pPerIO->ol,NULL) ; 
	}
 
	return 0 ;
}
 
//服務線程
DWORD WINAPI ServerThread(LPVOID lpParam) 
{
	//得到完成端口對象句柄`
	HANDLE hCompletion = (HANDLE)lpParam ;
	DWORD dwTrans ;
 
	PPER_HANDLE_DATA pPerHandle ;
	PPER_IO_DATA pPerIO ;
 
	while(TRUE)
	{
		//在關聯到此完成端口的所有套接字上等待I/O完成,倒數第二個參數的作法其實是非常不適當的
		BOOL bOK = GetQueuedCompletionStatus(hCompletion,&dwTrans,(LPDWORD)&pPerHandle,(LPOVERLAPPED*)&pPerIO,WSA_INFINITE) ;
 
		if(!bOK) //在此套接字上有錯誤發生
		{
			closesocket(pPerHandle->s) ;	
			GlobalFree(pPerHandle) ;
			GlobalFree(pPerIO) ;
			continue ;
		}
 
		//套接字被對方關閉
		if(dwTrans == 0 && (pPerIO->nOperationType == OP_READ || pPerIO->nOperationType == OP_WRITE))
		{
			closesocket(pPerHandle->s) ;
			GlobalFree(pPerHandle) ;
			GlobalFree(pPerIO) ;
			continue ;
		}
 
		switch(pPerIO->nOperationType)		//通過per-I/O數據中的nOperationType域查看什麼I/O請求完成了
		{
			case OP_READ : //完成一個接收請求
				{
					pPerIO->buf[dwTrans] = '\0' ;
					printf(pPerIO->buf) ;
 
					//繼續投遞接收I/O請求
					WSABUF buf ;
					buf.buf = pPerIO->buf ;
					buf.len = BUFFER_SIZE ;
 
					pPerIO->nOperationType = OP_READ ;
					DWORD nFlags = 0 ;
					WSARecv(pPerHandle->s,&buf,1,&dwTrans,&nFlags,&pPerIO->ol,NULL) ; //繼續引發在該套接字上面的接收操作
 
					//投遞一個發送請求I/O,我自己添加,爲了測試OR_WRITE之用
					PPER_IO_DATA pSendPerIO = (PPER_IO_DATA)GlobalAlloc(GPTR,sizeof(PER_IO_DATA)) ;
					pSendPerIO->nOperationType = OP_WRITE ;
					memcpy(pSendPerIO->buf,pPerIO->buf,dwTrans) ;
 
					WSABUF dbuf ;
					dbuf.buf = pSendPerIO->buf ;
					dbuf.len = dwTrans ;           //如果大於客戶端的接收緩衝區,則客戶端需要重複多次recv才能接收完所有數據
					WSASend(pPerHandle->s,&dbuf,1,&dwTrans,nFlags,&pSendPerIO->ol,NULL); //引發在該套接字上面的發送操作
				} 
				break ;
			
			case OP_WRITE :
				{
					printf("發送數據完成\n");
				}
				break ;
			case OP_ACCEPT : //這個多餘的,因爲主線程因爲在accpet了
				break ;
 
		}
	}
	return 0 ;
}

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