異步通信之IOCP詳解

一、 概述

學習完網絡基礎,在寫C/S應用程序時,大多童靴寫服務器基本都沒有用到io模型,基本都是採用“accept同步擁塞通訊和多線程方式”與客戶端通訊。但當有成千上萬客戶端請求連接並與服務器通訊時,多線程的創建與CPU上下文的切換,服務器端壓力可想而知,在資源有限的情況在,選擇一個好的io模型才能搭建高性能服務器。其中IOCP廣泛運用於個高性能服務器程序,apache服務器就是IOCP實現。

同步通訊和異步通信
在寫網絡程序時,我們知道CPU運行速度非常快,而在與IO設備進行數據交換時速度簡直不忍直視。在程序請求一個網絡操作,如accept,recv等時,若應用程序一直擁塞等待這些網絡操作結束返回結果後才接着執行後面的代碼,則這個過程就是同步通訊方式,反之,在提交網絡操作請求後,由系統去執行該請求,程序繼續往下走(該幹嘛幹嘛),待網絡請求操作完成後,系統在通知程序來處理結果,則就是異步通信方式。
所以異步通訊比同步通訊(accept+多線程)方式高效的多,IOCP通訊模型是比較好的網絡通訊模型。一起來學習吧!

二、IOCP執行流程
在這裏插入圖片描述

三、重要的數據結構

1.單句柄數據:該結構體用於管理具體哪個socket在進行io請求操作。

typedef struct PER_HANDLE_DATA{
	SOCKET 				socket;
	SOCKADDR_IN 		addr;
}*pPER_HANDLE_DATA,PER_HANDLE_DATA;

2.單IO數據:該結構體用於每一次客戶端socket向IOCP提交請求時提交給系統,在其結構內可定義任意參數(overlapped必須在第一個),當請求完成後,IOCP**“原封不動”**的返回到工作線程,但給相應參數進行了賦值,如用於接收數據的buffer數組。

typedef struct PER_IO_DATA{
	OVERLAPPED       overlapped;        //類似id,每個io都必須有一個
	SOCKET			 socket;            //io請求的套接字
	WSABUF			 wsabuf;            //用於從緩衝區獲取數據的結構
	char			 buffer[LENGTH];    //保存獲得的數據
	OPT_TYPE         opt_type;          //這次io請求的類型,如ACCEPT,RECV,SEND等
}*pPER_IO_DATA,PER_IO_DATA;

解釋一下WSABUF結構體,在ws2def.h中定義:

typedef struct _WSABUF{
	ULONG len;                            /*the length of buffer*/
	__field_bcount(len) CHAR FAR *buf;    /*  the pointer to the buffer  */
};

四、IOCP詳細實現

1、初始化listenSocket等相應庫、創建完成端口、創建工作線程。
(1) 創建listenSocket
既然我們使用的是IOCP模型,那麼創建的socket必須這樣創建,用於異步請求:

SOCKET socket = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
//socket只需要在服務端創建,客戶端不需要

(2) 創建完成端口

對,就是這麼簡單一句代碼就將我們完成端口創建完畢了,參數一個-1三個0,簡單吧!不過,你懂得,操作系統爲我們做了很多。。。

HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE,NULL,0,0);
//最後一個0比較有討論價值,它表示爲了避免CPU上下文切換,創建的工作線程最理想的狀態就是一個處理器一個線程。

(3)創建工作線程
完成端口之所以比較高效,能很好處理高併發訪問的原因之一就是合理的創建處理請求連接、收發數據等工作線程。

//工作線程數 = CPU數 * 2 + 1
//上面不是說理想狀態有多少個處理器創建多少工作線程麼,但現實是創建兩倍的線程比較高效
//因爲萬一有個線程被掛起了嘞,那不是有個處理器沒有被充分的利用了麼,得有個“備胎”啊,哈哈

//獲得cpu數
SYSTEM_INFO si;
int processors = si.dwNumerOfProcessors;

//創建工作線程
HANDLE * workThreadH =  new HANDLE[processors];
for(int number = 0 ; number < processors;number++){
	workThreadH[number] = (HANDLE) __beginthreadex(NULL,0,workThread,傳值,0,NULL);
}

2、將listenSocket與完成端口綁定,並投遞第一個accept請求,這樣完成端口就可以爲我們處理請求了。

//結構原型
HANDLE WINAPI CreateIoCompletionPort(  
    __in         HANDLE  FileHandle,               // 監聽套接字句柄  
    __in_opt     HANDLE  ExistingCompletionPort,   // 前面創建的完成端口  
    __in         ULONG_PTR CompletionKey,         
                                                   // 綁定自定義的結構體PRE_IO_DATA,傳遞到Worker線程中,相當於參數的傳遞  
     __in        DWORD NumberOfConcurrentThreads  // 置0  
); 
//綁定
//投遞第一個acceptEx請求
acceptEx(...);

3、投遞AcceptEx
完成端口使用的是AcceptEx接收客戶端連接,而不是accept。但AcceptEx比較特別,是微軟爲重疊I/0機制提供的函數。在mswsock.dll中提供,所以可以通過mswsock.lib靜態鏈接庫獲得AcceptEx。但是 但是 但是不推薦這樣獲取,而是應該用WSAIoctl來獲取AcceptEx指針,因爲沒有獲得指針的情況下就是用AcceptEx的開銷很大,並且不適用所用平臺。

//1、獲取AcceptEx
LPFN_ACCEPTEX 	pAcceptEx;			//AcceptEx指針
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes = 0;
WSAIoctl(
	socket,//只要有效的socket就可以
	SIO_GET_EXTENSION_FUNCTION_POINTER,
	&GuidAcceptEx,
	sizeof(GuidAcceptEx),
	&pAcceptEx,
	sizeof(pAcceptEx),
	&dwBytes,
	NULL,
	NULL
);
//2調用AcceptEx,投遞請求
//這裏向IOCP投遞的AcceptEx請求,需要綁定剛纔的PER_IO_DATA數據結構,當IOCP處理好請求返回後,我們將在work線程中獲得這個已經賦值好了的結構體,並且最重要的是在那麼多的返回請求中是通過overlapped來標記的,要不你怎麼去找到你現在投遞的結構體^_^。
bool AcceptEx(
	SOCKET       listenSocket,//監聽socket
	SOCKET       clientSocket,//提前創建好的接收連接的socket,這是AcceptEx高效性的關鍵
	PVOID        lpOutputBuffer,//接收緩衝區,第一部分是client發來的第一組數據,第二是服務端地址,第三是客戶端地址
	DWORD        dwReceiveDataLength,//lpOutputBuffer的長度,若不爲0,則表示等待客戶端第一組數組發過來後才返回,否則阻塞在這裏,若爲0,則表示不用等待,直接返回。
	DWORD        dwLocalAddressLength,//存放本地地址的大小
	DWORD        dwRemoteAddressLength,//存放遠程地址的大小
	LPVOID       lpdwBytesReceived,//不用管
	LPOVERLAPPED lpOverlapped//I/O重疊結構
);


4、投遞WSARecv請求

//傳遞參數,調用就行
int WSARecv(  
    SOCKET socket,                       // 投遞的套接字  
     LPWSABUF lpBuffers,                 // 接收緩衝區WSABUF結構構成的數組  
     DWORD dwBufferCount,                // 數組中WSABUF結構的數量,設置爲1  
     LPDWORD lpNumberOfBytesRecvd,       // 返回函數調用所接收到的字節數  
     LPDWORD lpFlags,                    // 設置爲0  
     LPWSAOVERLAPPED lpOverlapped,       // Socket對應的重疊結構  
     NULL                                // 設置完成例程模式,這裏設置爲NULL 
); 

5、投遞WSASend請求
//和WSARecv類似,不解釋

int WSASend(
  _In_   SOCKET			 socket,
  _In_   LPWSABUF		 lpBuffers,
  _In_   DWORD 			 dwBufferCount,
  _Out_  LPDWORD		 lpNumberOfBytesSent,
  _In_   DWORD			 dwFlags,
  _In_   LPWSAOVERLAPPED lpOverlapped,
  _In_   LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

6、work線程工作機制
work線程主要監聽完成端口的狀態(GetQueuedCompletionStatus),若完成端口有返回請求,則處理,若沒有則掛起。

函數原型如下:

BOOL WINAPI GetQueuedCompletionStatus(  
    __in   HANDLE          CompletionPort,    // 建立的完成端口  
    __out  LPDWORD         lpNumberOfBytes,   //返回的字節數  
    __out  PULONG_PTR      lpCompletionKey,   // 與完成端口的時候綁定的那個socket對應的自定義結構體PER_HANDLE_DATA
    __out  LPOVERLAPPED    *lpOverlapped,     // 重疊結構  
    __in   DWORD           dwMilliseconds     // 等待完成端口的超時時間,一般設置INFINITE  
    ); 

(1)監聽GetQueuedCompletionStatus

pPER_HANDLE_DATA 			 *pPerHandleData = NULL;  
OVERLAPPED        *pOverlapped = NULL;  
DWORD              dwBytesTransfered = 0;  
  
BOOL bReturn  =  GetQueuedCompletionStatus(  
                                     pIOCPModel->m_hIOCompletionPort,  
                               &dwBytesTransfered,  
                                     (LPDWORD)&pPerHandleData,  
                                    &pOverlapped,  
                                    INFINITE );  

(2)獲得PPER_IO_DATA
通過宏CONTAINING_RECORD獲得,將lpOverlapped變量傳到宏中,找到和結構體PER_IO_DATA中overlapped成員對應的數據,就是剛纔投遞PER_IO_DATA結構體,現在請求IO操作完成,返回給work線程的。

PPER_IO_DATA  pPerIoData = CONTAINING_RECORD(lpOverlapped, PER_IO_DATA  , overlapped);

(3)switch判斷完成請求的類型,然後做相應的處理
大概邏輯代碼如下:是不是太抽象了,具體代碼看後面更新的整體工程代碼

switch(pPerIoData.opt_type){
	case OPT_ACCEPT:
				//這裏大概做兩件是
				//一是將連接的socket與IOCP綁定
				//二是在接着投遞下一個AcceptEx,這樣AcceptEx接循環起來了,可以不斷的監聽。
				break;
	case OPT_RECV:
				//處理接收到的來自客戶端的數據,然後在投遞WSARecv請求
				//當然若還沒有完全接收完客戶端的一組數據,那麼得在投遞WASRecv請求,而不做任何數據處理。
				//比如若客戶端發送了1000個字節,但是現在dwBytesTransfered參數值爲800,則說明還有200字節還在緩衝區中,得繼續去緩衝區中取數據
				break;
	case OPT_SEND:
				//這裏主要是服務端向客戶端發送數據返回後,處理各種情況
				//1、若向客戶端發送的數據全部發送完畢,那麼釋放PER_IO_DATA結構所佔內存,服務器長時間運行,不可能讓內存溢出崩潰,是不。
				//2、若向客戶端發送的數據部分發送完畢,則需要在投遞WSAsend請求,將數據發送完畢
				break;
}

7、關閉完成端口
work線程創建後一直在while循環中監聽完成端口狀態,要麼處於掛起狀態,要麼處於處理數據狀態,那麼當要關閉服務器時,如何讓work線程溫文爾雅的退出嘞?我們知道讓線程安全退出的最好方法是讓線程自己退出,即return。
使用PostQueuedCompletionStatus通知線程退出:

BOOL WINAPI PostQueuedCompletionStatus(  
                   __in      HANDLE CompletionPort,  //當初創建的完成端口
                   __in      DWORD dwNumberOfBytesTransferred, //可做爲通知線程退出的一個標示碼,其對應於GetQueuedCompletionStatus中的參數lpNumberOfBytes,所以可做文章
                   __in      ULONG_PTR dwCompletionKey,  //PER_HANDLE_DATA結構體
                   __in_opt  LPOVERLAPPED lpOverlapped  
);

這是我對IOCP簡單的理解,若有不對的地方,希望各位指正,相互交流,共同成長_

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