IOCP 完成端口技術
完成端口技術,IOCP(complete port)
就是系統幫你完成網絡IO操作,在客戶端極多的情況下,這種模型效率很高。
多線程模型下:每個客戶端都分配一個線程的話,那麼CPU會把大部分時間片都浪費在線程之間的調度上,而不是每個線程中對網絡數據的處理上。
而I/O重疊模型讓CPU的工作更多集中在網絡數據的處理而非線程調度。
一個很有趣的比喻:IOCP技術相當於——老闆派祕書去前臺接待一個客人(WSARecv),然後繼續做老闆的事情,祕書會一直等待資料夾有新文件然後拿給你然後繼續等待有新文件(loop循環),前臺接待客人之後把客戶資料放到資料夾之後就不會繼續去前臺接待客人了(接收到了I/O完成確認),這個時候如果你不再派前臺繼續去等待接待客人(WSARecv),那麼你的資料夾不會有新的資料了,這時就需要再次指派前臺去等待接待新的客人(再次WSARecv)
WSASocket()
先來看與socket()的對比:
- WSASocket是Windows專用,支持異步操作;
- socket是unix標準,只能同步操作;
Socket()爲了實現非阻塞,可以用多線程實現;socket() 函數創建一個通訊端點並返回一個套接口,在應用於阻塞套接口時會阻塞。
WSASocket()是socket的windows平臺的實現,是微軟專門爲windows操作系統開發的socket網絡編程接口,而socket是通用的網絡編程接口,其發送操作和接收操作都可以被重疊使用:接收函數可以被多次調用,發出接收緩衝區,準備接收到來的數據;發送函數也可以被多次調用,組成一個發送緩衝區隊列。而socket()卻只能發過之後等待回消息纔可做下一步操作。
執行重疊I/O的WSASend()和WSARecv()
send()一次之可以發送一個緩衝區,對於發送大量的數據或數據包時會造成性能低下(多次調用send()造成從用戶態到核心套的轉換),解決方法是減少send()調用次數(申請一個大點的BUF緩衝區一次性發送數據),但這增加了編碼的工作。
WSASend()可以支持一次發送多個BUFFER的請求,每個被髮送的數據被填充到WSABUF結構中,然後傳遞給WSASend函數同時提供BUF的數量,這樣WSASend就能上面的工作而減少send的調用次數,從而提高了性能。
recv()和WSARecv()大同小異,只是功能上有實質的區分。
重疊I/O的I/O完成確認
爲了實現重疊I/O,WSASend和WSARecv函數執行後就立即返回了(異步),其他事情(什麼時候執行完畢,什麼時候開始執行)交給系統來管,進行重疊I/O的I/O完成確認很關鍵。
函數 WSAGetOverlappedResult()就是爲了實現I/O的完成確認存在的:
BOOL WSAGetOverlappedResult(
SOCKET S, //進行重疊I/O的套接字句柄
LPWSAOVERLAPPED lpOverlapped, //進行重疊I/O傳遞的WSAOVERLAPPED結構體變量的地址
LPDWORD lpcbTransfer, //用於保存實際傳輸自己數的變量地址值
BOOL fwait, //該函數正在進行I/O,fwait等待I/O完成,該值爲false時跳出函數
LPDWORD lpdwFlags //調用WSARecv時,用於獲取附加信息,若不需要傳NULL
)
創建“完成端口”的實現步驟及其相關函數
IOCP中已完成的I/O信息將註冊到完成端口對象(CP對象),但這個過程並非單純的註冊:當該套接字的I/O完成時,要把狀態信息註冊到指定CP對象(一般情況下,我們需要且僅需要一個CP對象)。
爲了完成上面的註冊請求,需要程序員:
- 創建完成端口對象(CP對象)
- 建立完成端口對象和套接字間的聯繫
這兩個工作都用CreateIoCompletionPort()函數完成:
一.創建一個完成端口:CreateIoCompletionPort()函數
HANDLE CreateIoCompletionPort(
HANDLE fileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
1.創建CP對象
fileHandle傳INVALID_HANDLE_VALUE;
ExistingCompletionPort傳NULL;
CompletionKey傳0;
NumberOfConcurrentThreads傳分配給該CP對象同時可運行的線程數,若傳0則線程數等於CPU個數。
以上調用方法是爲了建立完成端口
按這種傳參方法,調用這個函數,Windows系統會爲我們把完成端口的所有一切的東西后臺給我們弄好,然後返回給我們一個HANDLE,只要這個HANDLE不是NULL,就證明CP對象成功創建。
二.根據系統中CPU核心數量建立對應的Worker線程
獲取CPU數量:
SYSTEM_INFO si;
GetSystemInfo(&si);
int m_nProcessors = si.dwNumberOfProcessors;
獲取CPU數量後,創建CPU數量X2倍的線程數(兩倍是爲了充分利用CPU資源):
m_nThreads = 2 * m_nProcessors;
HANDLE* m_phWorkerThreads = new HANDLE[m_nThreads];
for (int i = 0; i < m_nThreads; i++)
{
m_phWorkerThreads[i] = ::CreateThread(0, 0, _WorkerThread, …);
}
三.創建一個SOCKET用於監聽,並在指定端口上監聽連接請求
CP端口創建完畢,我們要用這個CP端口來完成網絡通信。
3.1先初始化一個SOCKET:
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData);
struct sockaddr_in ServerAddress;
SOCKET m_sockListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
//I/O重疊 使用WSASocket()
ZeroMemory((char *)&ServerAddress, sizeof(ServerAddress));
ServerAddress.sin_family = AF_INET;
ServerAddress.sin_addr.s_addr = htonl(INADDR_ANY);
ServerAddress.sin_port = htons(8888);
if (SOCKET_ERROR == bind(m_sockListen, (struct sockaddr *) &ServerAddress, sizeof(ServerAddress)))
listen(m_sockListen,SOMAXCONN))
//SOMAXCONN是最大監聽隊列長度(Maximum queue length specifiable by listen)
//你也可以自己自定義指定長度
3.2連接CP對象和SOCKET
HANDLE CreateIoCompletionPort(
HANDLE fileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
fileHandle 傳 要連接的套接字句柄;
ExistingCompletionPort 傳 連接的CP對象句柄;
CompletionKey 傳 已完成的I/O相關信息(GetQueuedCompletionStatus函數有關);
NumberOfConcurrentThreads 傳 無亂傳遞什麼,只要該函數第二個參數非NULL則會被忽略。
以上調用方法不是爲了建立完成端口,而是爲了將新連入的SOCKET與完成端口綁定在一起。
至此SOCKET初始化完畢,可以使用這個SOCKET投遞AcceptEX請求。