文章目錄
當應用程序必須一次管理多個套接字時,完成端口模型提供了最好的系統性能。這個模型也提供了最好的伸縮性,它非常適合用來處理上百上千個套接字。
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 ;
}