I/O模型——完成端口
設計目的:
常見的網絡通信分為兩種:同步和異步。
在同步通信中,每一次接受數據都會導致主線程的掛起,從而阻塞住了其他操作。為了解決這一問題,我們通常會采取同步通信+多線程的策略,即為每一個連入的Socket分配一個線程。然而隨着連入的Socket的數量的增加,線程的數量也在增加,這樣CPU則需要不停地進行線程的切換,因此難以成為高性能的服務器程序。
異步通信則可以把接收數據這一操作交給內核,即在內核接收數據的時候,主線程可以不用被阻塞並且繼續執行其他操作,而一旦接收數據完成以后,再由內核通知主線程。而如何通知主線程是一個關鍵,不同的異步通信策略有着不同的通知方式。
在這樣的情況下,完成端口這一I/O模型被提出,成為目前Windows下性能最好的I/O模型之一。
實現原理:
首先根據CPU數量開好線程,當有用戶請求的時候,把這些請求加入一個特定的消息隊列中,而事先開好的線程則會排隊從這個消息隊列中獲取請求並作出處理。完成端口正是指這一消息隊列.
基本流程:

主要的API:
- 創建完成端口
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0 );
HANDLE WINAPI CreateIoCompletionPort(
_in_ HANDLE FileHandle,
// Socket的句柄,置為INVALID_HANDLE_VALUE表示創建一個沒有和任何HANDLE有關系的完成端口
_in_opt HANDLE ExistingCompletionPort,
// NULL表示新建一個完成端口
_in_ ULONG_PTR CompletionKey,
// 完成鍵,創建完成端口時置為0
_in_ DWORD NumberOfConcurrentThreads
// 完成端口並發線程的數量,置0表示有多少個CPU就開多少個線程
);
- 創建監聽Socket
初始化Socket庫...
...
listenSoc = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
...
綁定端口,並監聽...
- 將監聽的Socket綁定到完成端口上,這里同樣使用
HANDLE WINAPI CreateIoCompletionPort(...)這一API.
CreateIoCompletionPort(listenSoc, iocp, CompKey, 0);
HANDLE WINAPI CreateIoCompletionPort(
_in_ HANDLE FileHandle,
// 監聽Socket的句柄
_in_opt HANDLE ExistingCompletionPort,
// 剛才創建的完成端口
_in_ ULONG_PTR CompletionKey,
// 完成鍵,我們在綁定的同時為其分配一段內存空間,以存儲與這一Socket相關的信息,當網絡操作完成的時候,我們可以根據這段內存空間里面的信息分辨這是哪一個Socket
_in_ DWORD NumberOfConcurrentThreads
// 完成端口並發線程的數量,置0表示有多少個CPU就開多少個線程
);
- 在監聽端口上投遞AcceptEX請求
AcceptEX與傳統的Accept有三個主要不同點:
- AcceptEX采取異步方式,可以同時投遞多個請求,而Accept采取阻塞的方式,依次只能處理一個請求。
- AcceptEX會事先准備好Socket,當用戶請求連入的時候直接使用這一新的Socket,避免臨時創建Socket。
- AcceptEX接受連入請求的同時,我們可以附加一些數據,這樣我們就可以在接受用戶連入的同時,接受來自用戶的第一組數據。
BOOL AcceptEx (
SOCKET sListenSocket, // 監聽Socket
SOCKET sAcceptSocket, // 事先准備好給新用戶的Socket
PVOID lpOutputBuffer, // 接受緩沖區
DWORD dwReceiveDataLength, // 用於存放用戶第一組數據的空間大小
DWORD dwLocalAddressLength, // 本地地址的空間大小
DWORD dwRemoteAddressLength, // 客戶端地址的空間大小
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
// 重疊結構,每一個網絡操作都會對應一個重疊結構,相當於網絡操作的ID
);
- 投遞接受數據請求
int WSARecv(
SOCKET s, // 接受數據的Socket
LPWSABUF lpBuffers, // 接收緩沖區
DWORD dwBufferCount, // 置為1
LPDWORD lpNumberOfBytesRecvd, // 所接收到的字節數
LPDWORD lpFlags, // 置為0
LPWSAOVERLAPPED lpOverlapped, // 這個Socket對應的重疊結構
NULL
);
- 解析AcceptEX接收到的數據
AcceptEX緩沖區里面保存着本地地址,客戶端地址以及客戶端發來的第一組數據,因此我們需要使用GetAcceptExSockAddrs()來解析這些數據.
void GetAcceptExSockaddrs(
_In_ PVOID lpOutputBuffer,
// AcceptEX中的緩沖區
_In_ DWORD dwReceiveDataLength,
// 用戶第一組數據的空間大小
_In_ DWORD dwLocalAddressLength,
// 本地地址的空間大小
_In_ DWORD dwRemoteAddressLength,
// 客戶端地址的空間大小
_Out_ LPSOCKADDR *LocalSockaddr,
// 本地地址
_Out_ LPINT LocalSockaddrLength,
// 實際本地地址的空間大小
_Out_ LPSOCKADDR *RemoteSockaddr,
// 客戶端地址
_Out_ LPINT RemoteSockaddrLength
// 實際客戶端地址的大小
);
參考: 1. 完成端口(CompletionPort)詳解 - 手把手教你玩轉網絡編程系列之三
2. Overlapped模型深入分析

