WinSock2程式設計之打造完整的SOCKET池

ForTechnology發表於2011-08-16

WinSock2程式設計之打造完整的SOCKET池
部落格分類:
• IOCP
程式設計SocketWindows應用伺服器網路應用
WinSock2程式設計之打造完整的SOCKET池

IOCP程式設計 2010-02-15 22:46:34 閱讀592 評論5   字號:大中小 訂閱

 


在Winodows平臺上,網路程式設計的主要介面就是WinSock,目前大多數的Windows平臺上的WinSock平臺已經升級到2.0版, 簡稱為WinSock2。在WinSock2中擴充套件了很多很有用的Windows味很濃的SOCKET專用API,為Windows平臺使用者提供高效能的 網路程式設計支援。這些函式中的大多數已經不再是標準的“Berkeley”套接字模型的API了。使用這些函式的代價就是你不能再將你的網路程式輕鬆的移植 到“尤里平臺”(我給Unix +Linux平臺的簡稱)下,反過來因為Windows平臺支援標準的“Berkeley”套接字模型,所以你可以將大多數尤里平臺下的網路應用移植到 Windows平臺下。

如果不考慮可移植性(或者所謂的跨平臺性),而是著重於應用的效能時,尤其是注重伺服器效能時,對於Windows的程式,都鼓勵使用 WinSock2擴充套件的一些API,更鼓勵使用IOCP模型,因為這個模型是目前Windows平臺上比較完美的一個高效能IO程式設計模型,它不但適用於 SOCKET程式設計,還適用於讀寫硬碟檔案,讀寫和管理命名管道、郵槽等等。如果再結合Windows執行緒池,IOCP幾乎可以利用當今硬體所有可能的新特 性(比如多核,DMA,高速匯流排等等),本身具有先天的擴充套件性和可用性。

今天討論的重點就是SOCKET池。很多VC程式設計師也許對SOCKET池很陌生,也有些可能很熟悉,那麼這裡就先討論下這個概念。

在Windows平臺上SOCKET實際上被視作一個核心物件的控制程式碼,很多Windows API在支援傳統的HANDLE引數的同時也支援SOCKET,比如有名的CreateIoCompletionPort就支援將SOCKET控制程式碼代替 HANDLE引數傳入並呼叫。熟悉Windows核心原理的讀者,立刻就會發現,這樣的話,我們建立和銷燬一個SOCKET控制程式碼,實際就是在系統內部建立 了一個核心物件,對於Windows來說這牽扯到從Ring3層到Ring0層的耗時操作,再加上覆雜的安全稽核機制,實際建立和銷燬一個SOCKET內 核物件的成本還是蠻高的。尤其對於一些面向連線的SOCKET應用,服務端往往要管理n多個代表客戶端通訊的SOCKET物件,而且因為客戶的變動性,主 要面臨的大量操作除了一般的收發資料,剩下的就是不斷建立和銷燬SOCKET控制程式碼,對於一個頻繁接入和斷開的伺服器應用來說,建立和銷燬SOCKET的性 能代價立刻就會體現出來,典型的例如WEB伺服器程式,就是一個需要頻繁建立和銷燬SOCKET控制程式碼的SOCKET應用。這種情況下我們通常都希望對於斷 開的SOCKET物件,不是簡單的“銷燬”了之(很多時候“斷開”的含義不一定就等價於“銷燬”,可以仔細思考一下),更多時候希望能夠重用這個 SOCKET物件,這樣我們甚至可以事先建立一批SOCKET物件組成一個“池”,在需要的時候“重用”其中的SOCKET物件,不需要的時候將 SOCKET物件重新丟入池中即可,這樣就省去了頻繁建立銷燬SOCKET物件的效能損失。在原始的“Berkeley”套接字模型中,想做到這點是沒有 什麼辦法的。而幸運的是在Windows平臺上,尤其是支援WinSock2的平臺上,已經提供了一套完整的API介面用於支援SOCKET池。

對於符合以上要求的SOCKET池,首先需要做到的就是對SOCKET控制程式碼的“回收”,因為建立函式無論在那個平臺上都是現成的,而最早能夠實現 這個功能的WinSock函式就是TransmitFile,如果代替closesocket函式像下面這樣呼叫就可以“回收”一個SOCKET控制程式碼,而 不是銷燬:(注意“回收”這個功能對於TransmitFile函式來說只是個“副業”。)

TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );

注意上面函式的最後一個引數,使用了標誌TF_DISCONNECT和TF_REUSE_SOCKET,第一個值表示斷開,第二個值則明確的表示 “重用”實際上也就是回收這個SOCKET,經過這個處理的SOCKET控制程式碼,就可以直接再用於connect等操作,但是此時我們會發現,這個回收來的 SOCKET似乎沒什麼用,因為其他套接字函式沒法直接利用這個回收來的SOCKET控制程式碼。

這時就要WinSock2的一組專用API上場了。我將它們按傳統意義上的服務端和客戶端分為兩組:

一、         服務端:

SOCKET WSASocket(

  __in          int af,

  __in          int type,

  __in          int protocol,

  __in          LPWSAPROTOCOL_INFO lpProtocolInfo,

  __in          GROUP g,

  __in          DWORD dwFlags

);

BOOL AcceptEx(

  __in          SOCKET sListenSocket,

  __in          SOCKET sAcceptSocket,

  __in          PVOID lpOutputBuffer,

  __in          DWORD dwReceiveDataLength,

  __in          DWORD dwLocalAddressLength,

  __in          DWORD dwRemoteAddressLength,

  __out         LPDWORD lpdwBytesReceived,

  __in          LPOVERLAPPED lpOverlapped

);

BOOL DisconnectEx(

  __in          SOCKET hSocket,

  __in          LPOVERLAPPED lpOverlapped,

  __in          DWORD dwFlags,

  __in          DWORD reserved

);

二、         客戶端:

SOCKET WSASocket(

  __in          int af,

  __in          int type,

  __in          int protocol,

  __in          LPWSAPROTOCOL_INFO lpProtocolInfo,

  __in          GROUP g,

  __in          DWORD dwFlags

);

BOOL PASCAL ConnectEx(

  __in          SOCKET s,

  __in          const struct sockaddr* name,

  __in          int namelen,

  __in_opt      PVOID lpSendBuffer,

  __in          DWORD dwSendDataLength,

  __out         LPDWORD lpdwBytesSent,

  __in          LPOVERLAPPED lpOverlapped

);

BOOL DisconnectEx(

  __in          SOCKET hSocket,

  __in          LPOVERLAPPED lpOverlapped,

  __in          DWORD dwFlags,

  __in          DWORD reserved

);

注意觀察這些函式,似乎和傳統的“Berkeley”套接字模型中的一些函式“大同小異”,其實仔細觀察他們的引數,就已經可以發現一些呼叫他們的“玄機”了。

首先我們來看AcceptEx函式,與accept函式不同,它需要兩個SOCKET控制程式碼作為引數,頭一個引數的含義與accept函式的相同, 而第二個引數的意思就是accept函式返回的那個代表與客戶端通訊的SOCKET控制程式碼,在傳統的accept內部,實際在返回那個代表客戶端的 SOCKET時,是在內部呼叫了一個SOCKET的建立動作,先建立這個SOCKET然後再“accept”讓它變成代表客戶端連線的SOCKET,而 AcceptEx函式就在這裡“擴充套件”(實際上是“閹割”才對)accept函式,省去了內部那個明顯的建立SOCKET的動作,而將這個建立動作交給最 終的呼叫者自己來實現。AcceptEx要求呼叫者建立好那個sAcceptSocket控制程式碼然後傳進去,這時我們立刻發現,我們回收的那個SOCKET 是不是也可以傳入呢?答案是肯定的,我們就是可以利用這個函式傳入那個“回收”來的SOCKET控制程式碼,最終實現服務端的SOCKET重用。

這裡需要注意的就是,AcceptEx函式必須工作在非阻塞的IOCP模型下,同時即使AcceptEx函式返回了,也不代表客戶端連線進來或者 連線成功了,我們必須依靠它的“完成通知”才能知道這個事實,這也是AcceptEx函式區別於accept這個阻塞方式函式的最大之處。通常可以利用 AcceptEx的非阻塞特性和IOCP模型的優點,一次可以“預先”發出成千上萬個AcceptEx呼叫,“等待”客戶端的連線。對於習慣了 accept阻塞方式的程式設計師來說,理解AcceptEx的工作方式還是需要費一些周折的。下面的例子就演示瞭如何一次呼叫多個AcceptEx:

//批量建立SOCKET,並呼叫對應的AcceptEx

for(UINT i = 0; i < 1000; i++)

{//呼叫1000次

//建立與客戶端通訊的SOCKET,注意SOCKET的建立方式

skAccept = ::WSASocket(AF_INET,

                   SOCK_STREAM,

                   IPPROTO_TCP,

                   NULL,

                   0,

                   WSA_FLAG_OVERLAPPED);

if (INVALID_SOCKET == skAccept)

{

    throw CGRSException((DWORD)WSAGetLastError());

}

//建立一個自定義的OVERLAPPED擴充套件結構,使用IOCP方式呼叫

pAcceptOL = new CGRSOverlappedData(GRS_OP_ACCEPT

,this,skAccept,NULL);

pAddrBuf = pAcceptOL->GetAddrBuf();

//4、發出AcceptEx呼叫

//注意將AcceptEx函式接收連線資料緩衝的大小設定成了0,這將導致此函式立即返回,雖然與

//不設定成0的方式而言,這導致了一個較低下的效率,但是這樣提高了安全性,所以這種效率

//犧牲是必須的

if(!AcceptEx(m_skServer,

                   skAccept,

                   pAddrBuf->m_pBuf,

                   0,//將接收緩衝置為0,令AcceptEx直接返回,防止拒絕服務攻擊

                   GRS_ADDRBUF_SIZE,

                   GRS_ADDRBUF_SIZE,

                   NULL,

                   (LPOVERLAPPED)pAcceptOL))

{

int iError = WSAGetLastError();

if( ERROR_IO_PENDING != iError

     && WSAECONNRESET != iError )

{

     if(INVALID_SOCKET != skAccept)

     {

         ::closesocket(skAccept);

         skAccept = INVALID_SOCKET;

     }

     if( NULL != pAcceptOL)

     {

             GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));

delete pAcceptOL;

     pAcceptOL = NULL;

     }

  }

}

}

以上的例子只是簡單的演示了AcceptEx的呼叫,還沒有涉及到真正的“回收重用”這個主題,那麼下面的例子就演示瞭如何重用一個SOCKET控制程式碼:

if(INVALID_SOCKET == skClient)

{

throw CGRSException(_T("SOCKET控制程式碼是無效的!"));

}

OnPreDisconnected(skClient,pUseData,0);

CGRSOverlappedData*pData

= new GRSOverlappedData(GRS_OP_DISCONNECTEX

,this,skClient,pUseData);

//回收而不是關閉後再建立大大提高了伺服器的效能

DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0);

......

      //在接收到DisconnectEx函式的完成通知之後,我們就可以重用這個SOCKET了

CGRSAddrbuf*pBuf = NULL;

pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT

,this,skClient,pUseData);

pBuf = pNewOL->GetAddrBuf();

//把這個回收的SOCKET重新丟進連線池

if(!AcceptEx(m_skServer,skClient,pBuf->m_pBuf,

                 0,//將接收緩衝置為0,令AcceptEx直接返回,防止拒絕服務攻擊

                 GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE,

                 NULL,(LPOVERLAPPED)pNewOL))

{

int iError = WSAGetLastError();

    if( ERROR_IO_PENDING != iError

        && WSAECONNRESET != iError )

    {

        throw CGRSException((DWORD)iError);

     }

}

//注意在這個SOCKET被重新利用後,重新與IOCP繫結一下,該操作會返回一個已設定的錯誤,這個錯誤直接被忽略即可

::BindIoCompletionCallback((HANDLE)skClient

,Server_IOCPThread, 0);

 

至此回收重用SOCKET的工作也就結束了,以上的過程實際理解了IOCP之後就比較好理解了,例子的最後我們使用了 BindIoCompletionCallback函式重新將SOCKET丟進了IOCP執行緒池中,實際還可以再次使用 CreateIoCompletionPort函式達到同樣的效果,這裡列出這一步就是告訴大家,不要忘了再次繫結一下完成埠和SOCKET。

    對於客戶端來說,可以使用ConnectEx函式來代替connect函式,與AcceptEx函式相同,ConnectEx函式也是以非阻塞的IOCP 方式工作的,唯一要注意的就是在WSASocket呼叫之後,在ConnectEx之前要呼叫一下bind函式,將SOCKET提前繫結到一個本地地址端 口上,當然回收重用之後,就無需再次繫結了,這也是ConnectEx較之connect函式高效的地方之一。

   與AcceptEx函式類似,也可以一次發出成千上萬個ConnectEx函式的呼叫,可以連線到不同的伺服器,也可以連線到相同的伺服器,連線到不同的伺服器時,只需提供不同的sockaddr即可。

    通過上面的例子和講解,大家應該對SOCKET池概念以及實際的應用有個大概的瞭解了,當然核心仍然是理解了IOCP模型,否則還是寸步難行。

在上面的例子中,回收SOCKET控制程式碼主要使用了DisconnectEx函式,而不是之前介紹的TransmitFile函式,為什麼呢?因為 TransmitFile函式在一些情況下會造成死鎖,無法正常回收SOCKET,畢竟不是專業的回收重用SOCKET函式,我就遇到過好幾次死鎖,最後 偶然的發現了DisconnectEx函式這個專用的回收函式,呼叫之後發現比TransmitFile專業多了,而且不管怎樣都不會死鎖。

最後需要補充的就是這幾個函式的呼叫方式,不能像傳統的SOCKET API那樣直接呼叫它們,而需要使用一種間接的方式來呼叫,尤其是AcceptEx和DisconnectEx函式,下面給出了一個例子類,用於演示如何動態載入這些函式並呼叫之:

class CGRSMsSockFun

{

public:

CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)

{

     if( INVALID_SOCKET != skTemp )

     {

       LoadAllFun(skTemp);

      }

}

public:

virtual ~CGRSMsSockFun(void)

{

}

protected:

BOOL LoadWSAFun(SOCKET& skTemp,GUID&funGuid,void*&pFun)

{

     DWORD dwBytes = 0;

     BOOL bRet = TRUE;

     pFun = NULL;

     BOOL bCreateSocket = FALSE;

     try

     {

       if(INVALID_SOCKET == skTemp)

       {

          skTemp = ::WSASocket(AF_INET,SOCK_STREAM,

             IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);

bCreateSocket = (skTemp != INVALID_SOCKET);

       }

if(INVALID_SOCKET == skTemp)

       {

          throw CGRSException((DWORD)WSAGetLastError());

       }

       if(SOCKET_ERROR == ::WSAIoctl(skTemp,

                SIO_GET_EXTENSION_FUNCTION_POINTER,

                &funGuid,sizeof(funGuid),

                &pFun,sizeof(pFun),&dwBytes,NULL,

                NULL))

       {

             pFun = NULL;

             throw CGRSException((DWORD)WSAGetLastError());

       }

  }

  catch(CGRSException& e)

  {

      if(bCreateSocket)

      {

        ::closesocket(skTemp);

      }

  }

  return NULL != pFun;

}

protected:

LPFN_ACCEPTEX m_pfnAcceptEx;

LPFN_CONNECTEX m_pfnConnectEx;

LPFN_DISCONNECTEX m_pfnDisconnectEx;

LPFN_GETACCEPTEXSOCKADDRS m_pfnGetAcceptExSockaddrs;

LPFN_TRANSMITFILE m_pfnTransmitfile;

LPFN_TRANSMITPACKETS m_pfnTransmitPackets;

LPFN_WSARECVMSG m_pfnWSARecvMsg;

protected:

BOOL LoadAcceptExFun(SOCKET &skTemp)

{

     GUID GuidAcceptEx = WSAID_ACCEPTEX;

     return LoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnAcceptEx);

}

BOOL LoadConnectExFun(SOCKET &skTemp)

{

     GUID GuidAcceptEx = WSAID_CONNECTEX;

     return LoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnConnectEx);

}

BOOL LoadDisconnectExFun(SOCKET&skTemp)

{

     GUID GuidDisconnectEx = WSAID_DISCONNECTEX;

     return LoadWSAFun(skTemp,GuidDisconnectEx

,(void*&)m_pfnDisconnectEx);

}

BOOL LoadGetAcceptExSockaddrsFun(SOCKET &skTemp)

{

     GUID GuidGetAcceptExSockaddrs

= WSAID_GETACCEPTEXSOCKADDRS;

     return LoadWSAFun(skTemp,GuidGetAcceptExSockaddrs

,(void*&)m_pfnGetAcceptExSockaddrs);

}

BOOL LoadTransmitFileFun(SOCKET&skTemp)

{

     GUID GuidTransmitFile = WSAID_TRANSMITFILE;

     return LoadWSAFun(skTemp,GuidTransmitFile

,(void*&)m_pfnTransmitfile);

}

BOOL LoadTransmitPacketsFun(SOCKET&skTemp)

{

     GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;

     return LoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnTransmitPackets);

}

BOOL LoadWSARecvMsgFun(SOCKET&skTemp)

{

     GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;

     return LoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnWSARecvMsg);

}

public:

BOOL LoadAllFun(SOCKET skTemp)

{//注意這個地方的呼叫順序,是根據伺服器的需要,並結合了表示式副作用

  //而特意安排的呼叫順序

  return (LoadAcceptExFun(skTemp) &&

             LoadGetAcceptExSockaddrsFun(skTemp) &&

             LoadTransmitFileFun(skTemp) &&

             LoadTransmitPacketsFun(skTemp) &&

             LoadDisconnectExFun(skTemp) &&

             LoadConnectExFun(skTemp) &&

             LoadWSARecvMsgFun(skTemp));

}

 

public:

GRS_FORCEINLINE BOOL AcceptEx (

          SOCKET sListenSocket,

          SOCKET sAcceptSocket,

          PVOID lpOutputBuffer,

          DWORD dwReceiveDataLength,

          DWORD dwLocalAddressLength,

          DWORD dwRemoteAddressLength,

          LPDWORD lpdwBytesReceived,

          LPOVERLAPPED lpOverlapped

          )

{

     GRS_ASSERT(NULL != m_pfnAcceptEx);

     return m_pfnAcceptEx(sListenSocket,

             sAcceptSocket,lpOutputBuffer,

             dwReceiveDataLength,dwLocalAddressLength,

             dwRemoteAddressLength,lpdwBytesReceived,

             lpOverlapped);

}

GRS_FORCEINLINE BOOL ConnectEx(

          SOCKET s,const struct sockaddr FAR *name,

          int namelen,PVOID lpSendBuffer,

          DWORD dwSendDataLength,LPDWORD lpdwBytesSent,

          LPOVERLAPPED lpOverlapped

          )

{

     GRS_ASSERT(NULL != m_pfnConnectEx);

     return m_pfnConnectEx(

             s,name,namelen,lpSendBuffer,

             dwSendDataLength,lpdwBytesSent,

             lpOverlapped

             );

}

GRS_FORCEINLINE BOOL DisconnectEx(

          SOCKET s,LPOVERLAPPED lpOverlapped,

          DWORD  dwFlags,DWORD  dwReserved

          )

{

     GRS_ASSERT(NULL != m_pfnDisconnectEx);

     return m_pfnDisconnectEx(s,

             lpOverlapped,dwFlags,dwReserved);

}

GRS_FORCEINLINE VOID GetAcceptExSockaddrs (

          PVOID lpOutputBuffer,

          DWORD dwReceiveDataLength,

          DWORD dwLocalAddressLength,

          DWORD dwRemoteAddressLength,

          sockaddr **LocalSockaddr,

          LPINT LocalSockaddrLength,

          sockaddr **RemoteSockaddr,

          LPINT RemoteSockaddrLength

          )

{

     GRS_ASSERT(NULL != m_pfnGetAcceptExSockaddrs);

     return m_pfnGetAcceptExSockaddrs(

          lpOutputBuffer,dwReceiveDataLength,

          dwLocalAddressLength,dwRemoteAddressLength,

          LocalSockaddr,LocalSockaddrLength,

          RemoteSockaddr,RemoteSockaddrLength

          );

}

GRS_FORCEINLINE BOOL TransmitFile(

     SOCKET hSocket,HANDLE hFile,

     DWORD nNumberOfBytesToWrite,

     DWORD nNumberOfBytesPerSend,

     LPOVERLAPPED lpOverlapped,

     LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,

     DWORD dwReserved

     )

{

     GRS_ASSERT(NULL != m_pfnTransmitfile);

     return m_pfnTransmitfile(

             hSocket,hFile,nNumberOfBytesToWrite,

             nNumberOfBytesPerSend,lpOverlapped,

             lpTransmitBuffers,dwReserved

             );

}

GRS_FORCEINLINE BOOL TransmitPackets(

     SOCKET hSocket,                           

     LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,                              

     DWORD nElementCount,DWORD nSendSize,              

     LPOVERLAPPED lpOverlapped,DWORD dwFlags                             

     )

{

     GRS_ASSERT(NULL != m_pfnTransmitPackets);

     return m_pfnTransmitPackets(

             hSocket,lpPacketArray,nElementCount,

nSendSize,lpOverlapped,dwFlags

             );

}

GRS_FORCEINLINE INT WSARecvMsg(

          SOCKET s,LPWSAMSG lpMsg,

          LPDWORD lpdwNumberOfBytesRecvd,

          LPWSAOVERLAPPED lpOverlapped,

          LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

          )

{

     GRS_ASSERT(NULL != m_pfnWSARecvMsg);

     return m_pfnWSARecvMsg(

             s,lpMsg,lpdwNumberOfBytesRecvd,

             lpOverlapped,lpCompletionRoutine

             );

}

/*WSAID_ACCEPTEX

  WSAID_CONNECTEX

  WSAID_DISCONNECTEX

  WSAID_GETACCEPTEXSOCKADDRS

  WSAID_TRANSMITFILE

  WSAID_TRANSMITPACKETS

  WSAID_WSARECVMSG

  WSAID_WSASENDMSG */

};

這個類的使用非常簡單,只需要宣告一個類的物件,然後呼叫其成員AcceptEx、DisconnectEx函式等即可,引數與這些函式的 MSDN宣告方式完全相同,除了本文中介紹的這些函式外,這個類還包含了很多其他的Winsock2函式,那麼都應該按照這個類中演示的這樣來動態載入後 再行呼叫,如果無法載入通常說明你的環境中沒有Winsock2函式庫,或者是你初始化的不是2.0版的Winsock環境。

這個類是本人完整類庫的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什麼問題,可以直接跟帖說明,我會不定期回答大家的問題, 這個類可以免費使用、分發、修改,可以用於任何商業目的,但是對於使用後引起的任何問題,本人概不負責,有問題請跟帖。關於AcceptEx以及其他一些 函式,包括本文中沒有介紹到得函式,我會在後續的一些專題文章中進行詳細深入的介紹,敬請期待。如果你有什麼疑問,或者想要了解什麼也請跟帖說明,我會在 後面的文章中儘量說明。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/25897606/viewspace-704942/,如需轉載,請註明出處,否則將追究法律責任。

相關文章