c# AcceptEx與完成端口(IOCP)結合的示例
前言
在windows平臺下實現(xiàn)高性能網(wǎng)絡服務器,iocp(完成端口)是唯一選擇。編寫網(wǎng)絡服務器面臨的問題有:
1 快速接收客戶端的連接。
2 快速收發(fā)數(shù)據(jù)。
3 快速處理數(shù)據(jù)。本文主要解決第一個問題。
AcceptEx函數(shù)定義
BOOL AcceptEx( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );
為什么要用AcceptEx
傳統(tǒng)的accept函數(shù)能滿足大部分場景的需要;但在某些極端條件下,必須使用acceptEx來實現(xiàn)。兩個函數(shù)的區(qū)別如下:
1)accept是阻塞的;在一個端口監(jiān)聽,必須啟動一個專用線程調(diào)用accept。當然也可以用迂回的方式,繞過這個限制,處理起來會很麻煩,見文章單線程實現(xiàn)同時監(jiān)聽多個端口。acceptEx是異步的,可以同時對很多端口監(jiān)聽(監(jiān)聽端口的數(shù)量沒有上限的限制)。采用迂回的方式,使用accept監(jiān)聽,一個線程最多監(jiān)聽64個端口。這一點可能不是AcceptEx最大優(yōu)點,畢竟同時對多個端口監(jiān)聽的情況非常少見。
2)AcceptEx可以返回更多的數(shù)據(jù)。a)AcceptEx可以返回本地和對方ip地址和端口;而不需要調(diào)用函數(shù)getsockname和getpeername獲取網(wǎng)絡地址了。b)AcceptEx可以再接收到一段數(shù)據(jù)后,再返回。這種做法有利有弊,一般不建議這樣做。
3)AcceptEx是先準備套接字(socket)后接收。為了應對突發(fā)的連接高峰,可以多次投放AcceptEx。accept是事后建立SOCKET,就是tcp三次握手完成后,accept調(diào)用才返回,再生成socket。生成套接字是相對比較耗時的操作,accept的方式無法及時處理突發(fā)連接。對于AcceptEx的處理方式為建議做如下處理:一個線程負責創(chuàng)建socket,一個線程負責處理AcceptEx返回。
以上僅僅通過文字說明了AcceptEx的特點。下面通過具體代碼,逐一剖析。我將AcceptEx的處理封裝到類IocpAcceptEx中。編寫該類時,盡量做到高內(nèi)聚低耦合,使該類可以方便的被其他模塊使用。
IocpAcceptEx外部功能說明
class IocpAcceptEx
{
public:
IocpAcceptEx();
~IocpAcceptEx();
//設置回調(diào)接口。當accept成功,調(diào)用回調(diào)接口。
void SetCallback(IAcceptCallback* callback);
// 增加監(jiān)聽端口
void AddListenPort(UINT16 port);
//啟動服務
BOOL Start();
void Stop();
。。。以下代碼省略
}
#define POST_ACCEPT 1
//使用IocpAcceptEx類,必須實現(xiàn)該接口。接收客戶端的連接
class IAcceptCallback
{
public:
virtual void OnAcceptClient(SOCKET hSocketClient, UINT16 nListenPort) = 0;
};
該類的調(diào)用函數(shù)很簡單,對外接口也很明確。說明該類的職責很清楚,這也符合單一職責原則。
實現(xiàn)步驟說明
AcceptEx不但需要與監(jiān)聽端口綁定,還需要與完成端口綁定。所以程序的第一步是創(chuàng)建完成端口:
a)創(chuàng)建完成端口
m_hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0); if (m_hIocp == NULL) return FALSE;
b)監(jiān)聽端口創(chuàng)建與綁定
//生成套接字
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;
}
//啟動監(jiān)聽
if (listen(serverSocket, SOMAXCONN) != 0)
{
closesocket(serverSocket);
serverSocket = INVALID_SOCKET;
return false;
}
//監(jiān)聽端口與完成端口綁定
if (CreateIoCompletionPort((HANDLE)serverSocket, m_hIocp, (ULONG_PTR)this, 0) == NULL)
{
closesocket(serverSocket);
serverSocket = INVALID_SOCKET;
return false;
}
c)投遞AcceptEx
struct AcceptOverlapped
{
OVERLAPPED overlap;
INT32 opType;
SOCKET serverSocket;
SOCKET clientSocket;
char lpOutputBuf[128];
DWORD dwBytes;
};
int IocpAcceptEx::NewAccept(SOCKET serverSocket)
{
//創(chuàng)建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;
//存放網(wǎng)絡地址的長度
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是非阻塞操作,調(diào)用會立即返回。當有客戶端連接時,怎么得到通知。答案是通過完成端口返回。注意有一個步驟:監(jiān)聽端口與完成端口綁定,就是serverSocket與m_hIocp綁定,所以當有客戶端連接serverSocket時,m_hIocp會得到通知。需要生成線程,等待完成端口的通知。
d)通過完成端口,獲取通知
DWORD dwBytesTransferred;
ULONG_PTR Key;
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;
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。
//使用線程池好像有點小題大做。前文已說過,套接字的創(chuàng)建相對是比較耗時的操作。
//如果不在線程池投遞AcceptEx,AcceptEx的優(yōu)點就被抹殺了。
ThreadPoolParam *param = new ThreadPoolParam();
param->pIocpAcceptEx = this;
param->ServeSocket = acceptData->serverSocket;
QueueUserWorkItem(AcceptExThreadPool, this, 0);
delete acceptData;
return 0;
}
后記
采用完成端口是提高IO處理能力的一個途徑(廣義上講,通訊操作也是IO)。為了提高IO處理能力,windows提供很多異步操作函數(shù),這些函數(shù)都與完成端口關聯(lián),所以這一類處理的思路基本一致。學會了AcceptEx的使用,可以做到觸類旁通的效果。
以上就是c# AcceptEx與完成端口(IOCP)結合的示例的詳細內(nèi)容,更多關于c# AcceptEx與完成端口(IOCP)結合的資料請關注腳本之家其它相關文章!

