AcceptEx與完成埠結合例項
前言 在windows平臺下實現高效能網路伺服器,iocp(完成埠)是唯一選擇。編寫網路伺服器面臨的問題有:1 快速接收客戶端的連線。2 快速收發資料。3 快速處理資料。本文主要解決第一個問題。
AcceptEx函式定義
BOOL AcceptEx( SOCKETsListenSocket, SOCKETsAcceptSocket, PVOIDlpOutputBuffer, DWORDdwReceiveDataLength, DWORDdwLocalAddressLength, DWORDdwRemoteAddressLength, LPDWORDlpdwBytesReceived, LPOVERLAPPED lpOverlapped );
為什麼要用AcceptEx
傳統的accept函式能滿足大部分場景的需要;但在某些極端條件下,必須使用acceptEx來實現。兩個函式的區別如下:
1)accept是阻塞的;在一個埠監聽,必須啟動一個專用執行緒呼叫accept。當然也可以用迂迴的方式,繞過這個限制,處理起來會很麻煩,見文章單執行緒實現同時監聽多個埠 。acceptEx是非同步的,可以同時對很多埠監聽(監聽埠的數量沒有上限的限制)。採用迂迴的方式,使用accept監聽,一個執行緒最多監聽64個埠。這一點可能不是AcceptEx最大優點,畢竟同時對多個埠監聽的情況非常少見。
2)AcceptEx可以返回更多的資料。a)AcceptEx可以返回本地和對方ip地址和埠;而不需要呼叫函式getsockname和getpeername獲取網路地址了。b)AcceptEx可以再接收到一段資料後,再返回。這種做法有利有弊,一般不建議這樣做。
3)AcceptEx是先準備套接字(socket)後接收。為了應對突發的連線高峰,可以多次投放AcceptEx。accept是事後建立SOCKET,就是tcp三次握手完成後,accept呼叫才返回,再生成socket。生成套接字是相對比較耗時的操作,accept的方式無法及時處理突發連線。對於AcceptEx的處理方式為建議做如下處理:一個執行緒負責建立socket,一個執行緒負責處理AcceptEx返回。
以上僅僅通過文字說明了AcceptEx的特點。下面通過具體程式碼,逐一剖析。我將AcceptEx的處理封裝到類IocpAcceptEx中。編寫該類時,儘量做到高內聚低耦合,使該類可以方便的被其他模組使用。
IocpAcceptEx外部功能說明
class IocpAcceptEx { public: IocpAcceptEx(); ~IocpAcceptEx(); //設定回撥介面。當accept成功,呼叫回撥介面。 void SetCallback(IAcceptCallback* callback); // 增加監聽埠 void AddListenPort(UINT16 port); //啟動服務 BOOL Start(); void Stop(); 。。。以下程式碼省略 } #define POST_ACCEPT 1 //使用IocpAcceptEx類,必須實現該介面。接收客戶端的連線 class IAcceptCallback { public: virtual void OnAcceptClient(SOCKET hSocketClient, UINT16 nListenPort) = 0; };
該類的呼叫函式很簡單,對外介面也很明確。說明該類的職責很清楚,這也符合單一職責原則。
實現步驟說明
AcceptEx不但需要與監聽埠繫結,還需要與完成埠繫結。所以程式的第一步是建立完成埠:
a)建立完成埠
m_hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0); if (m_hIocp == NULL) return FALSE;
b)監聽埠建立與繫結
//生成套接字 SOCKET serverSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); if (serverSocket == INVALID_SOCKET) { return false; } //繫結 SOCKADDR_IN addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr =INADDR_ANY ; addr.sin_port = htons(port); if (bind(serverSocket, (sockaddr *)&addr, sizeof(addr)) != 0) { closesocket(serverSocket); serverSocket = INVALID_SOCKET; return false; } //啟動監聽 if (listen(serverSocket, SOMAXCONN) != 0) { closesocket(serverSocket); serverSocket = INVALID_SOCKET; return false; } //監聽埠與完成埠繫結 if (CreateIoCompletionPort((HANDLE)serverSocket, m_hIocp, (ULONG_PTR)this, 0) == NULL) { closesocket(serverSocket); serverSocket = INVALID_SOCKET; return false; }
c)投遞AcceptEx
struct AcceptOverlapped { OVERLAPPEDoverlap; INT32 opType; SOCKET serverSocket; SOCKET clientSocket; char lpOutputBuf[128]; DWORD dwBytes; }; int IocpAcceptEx::NewAccept(SOCKET serverSocket) { //建立socket SOCKET _socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); AcceptOverlapped *ov = new AcceptOverlapped(); ZeroMemory(ov,sizeof(AcceptOverlapped)); ov->opType = POST_ACCEPT; ov->clientSocket = _socket; ov->serverSocket = serverSocket; //存放網路地址的長度 int addrLen = sizeof(sockaddr_in) + 16; int bRetVal = AcceptEx(serverSocket, _socket, ov->lpOutputBuf, 0,addrLen, addrLen, &ov->dwBytes, (LPOVERLAPPED)ov); if (bRetVal == FALSE) { int error = WSAGetLastError(); if (error != WSA_IO_PENDING) { closesocket(_socket); return 0; } } return 1; }
AcceptEx是非阻塞操作,呼叫會立即返回。當有客戶端連線時,怎麼得到通知。答案是通過完成埠返回。注意有一個步驟:監聽埠與完成埠繫結,就是serverSocket與m_hIocp繫結,所以當有客戶端連線serverSocket時,m_hIocp會得到通知。需要生成執行緒,等待完成埠的通知。
d)通過完成埠,獲取通知
DWORD dwBytesTransferred; ULONG_PTRKey; BOOL rc; int error; AcceptOverlapped *lpPerIOData = NULL; while (m_bServerStart) { error = NO_ERROR; rc = GetQueuedCompletionStatus( m_hIocp, &dwBytesTransferred, &Key, (LPOVERLAPPED *)&lpPerIOData, INFINITE); if (rc == FALSE) { error = 0; if (lpPerIOData == NULL) { DWORD lastError = GetLastError(); if (lastError == WAIT_TIMEOUT) { continue; } else { assert(false); return lastError; } } } if (lpPerIOData != NULL) { switch (lpPerIOData->opType) { case POST_ACCEPT: { OnIocpAccept(lpPerIOData, dwBytesTransferred, error); } break; } } else { } } return 0;
<strong> </strong>
DWORD WINAPI IocpAcceptEx::AcceptExThreadPool(PVOID pContext) { ThreadPoolParam *param = (ThreadPoolParam*)pContext; param->pIocpAcceptEx->NewAccept(param->ServeSocket); delete param; return 0; } int IocpAcceptEx::OnIocpAccept(AcceptOverlapped *acceptData, int transLen, int error) { m_IAcceptCallback->OnAcceptClient(acceptData->clientSocket, acceptData->serverSocket); //當一個AcceptEx返回,需要投遞一個新的AcceptEx。 //使用執行緒池好像有點小題大做。前文已說過,套接字的建立相對是比較耗時的操作。 //如果不線上程池投遞AcceptEx,AcceptEx的優點就被抹殺了。 ThreadPoolParam *param = new ThreadPoolParam(); param->pIocpAcceptEx = this; param->ServeSocket = acceptData->serverSocket; QueueUserWorkItem(AcceptExThreadPool, this, 0); delete acceptData; return 0; }
後記 採用完成埠是提高IO處理能力的一個途徑(廣義上講,通訊操作也是IO)。為了提高IO處理能力,windows提供很多非同步操作函式,這些函式都與完成埠關聯,所以這一類處理的思路基本一致。學會了AcceptEx的使用,可以做到觸類旁通的效果。