c# AcceptEx與完成端口(IOCP)結合的示例

前言

在windows平臺下實現高性能網絡服務器,iocp(完成端口)是唯一選擇。編寫網絡服務器面臨的問題有:

1 快速接收客戶端的連接。

2 快速收發數據。

3 快速處理數據。本文主要解決第一個問題。

AcceptEx函數定義

BOOL AcceptEx(
 SOCKET  sListenSocket,
 SOCKET  sAcceptSocket,
 PVOID  lpOutputBuffer,
 DWORD  dwReceiveDataLength,
 DWORD  dwLocalAddressLength,
 DWORD  dwRemoteAddressLength,
 LPDWORD  lpdwBytesReceived,
 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
{
 OVERLAPPED  overlap;
 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_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。 
 //使用線程池好像有點小題大做。前文已說過,套接字的創建相對是比較耗時的操作。
 //如果不在線程池投遞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的使用,可以做到觸類旁通的效果。

以上就是c# AcceptEx與完成端口(IOCP)結合的示例的詳細內容,更多關於c# AcceptEx與完成端口(IOCP)結合的資料請關註WalkonNet其它相關文章!

推薦閱讀: