天天看點

IOCP+WinSock2新函數打造高性能SOCKET池

<a href="http://gamebabyrocksun.blog.163.com/blog/static/57153463201021554716831/">http://gamebabyrocksun.blog.163.com/blog/static/57153463201021554716831/</a>

首先這裡要重點重申一下就是,SOCKET池主要指的是使用面向連接配接的協定的情況下,最常用的就是需要管理大量的TCP連接配接的時候。常見的就是Web伺服器、FTP伺服器等。

下面就分步驟的詳細介紹如何最終實作SOCKET池。

一、WinSock2環境的初始化:

要使用WinSock2就需要先初始化Socket2.0的環境,不廢話,上代碼:

WSADATA wd = {0};

int iError = WSAStartup(MAKEWORD(2,0), &amp;wd);

if( 0 != iError )

{//出現錯誤,最好跟蹤看下錯誤碼是多少

       return FALSE;

}

if ( LOBYTE(lpwsaData-&gt;wVersion) != 2 )

{//非2.0以上環境 退出了事 可能是可憐的WinCE系統

       WSACleanup();

最後再不使用WinSock之後都要記得調用一下WSACleanup()這個函數;

二、裝載WinSock2函數:

上一篇文章中給出了一個裝載WinSock2函數的類,這裡分解介紹下裝載的具體過程,要提醒的就是,凡是類裡面示範了動态裝載的函數,最好都像那樣動态載入,然後再調用。以免出現上網發帖跪求高手賜教為什麼AcceptEx函數無法編譯通過等問題。看完這篇文章詳細你不會再去發帖找答案了,呵呵呵,好了,上代碼:

//定義一個好用的載入函數 摘自CGRSMsSockFun 類

BOOL LoadWSAFun(GUID&amp;funGuid,void*&amp; pFun)

{//本函數利用參數傳回函數指針

       DWORD dwBytes = 0;

       pFun = NULL;

       //随便建立一個SOCKET供WSAIoctl使用 并不一定要像下面這樣建立

       SOCKET skTemp = ::WSASocket(AF_INET,

                     SOCK_STREAM, IPPROTO_TCP, NULL,

                     0, WSA_FLAG_OVERLAPPED);

       if(INVALID_SOCKET == skTemp)

       {//通常表示沒有正常的初始化WinSock環境

              return FALSE;

       }

       ::WSAIoctl(skTemp, SIO_GET_EXTENSION_FUNCTION_POINTER,

                     &amp;funGuid,sizeof(funGuid),&amp;pFun,

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

       ::closesocket(skTemp);

       return NULL != pFun;

//示範如何動态載入AcceptEx函數

......

LPFN_ACCEPTEX pfnAcceptEx; //首先聲明函數指針

GUID GuidAcceptEx = WSAID_ACCEPTEX;

LoadWSAFun(GuidAcceptEx,(void*&amp;)pfnAcceptEx); //載入

//使用豐富的參數調用

pfnAcceptEx(sListenSocket,sAcceptSocket,lpOutputBuffer,

              dwReceiveDataLength,dwLocalAddressLength,dwRemoteAddressLength,

lpdwBytesReceived,lpOverlapped);

              //或者:

              SOCKET skAccept = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,

                                                 NULL, 0,WSA_FLAG_OVERLAPPED);

              PVOID pBuf = new BYTE[sizeof(sockaddr_in) + 16];

              pfnAcceptEx(skServer, skAccept,pBuf,

0,//将接收緩沖置為0,令AcceptEx直接傳回,防止拒絕服務攻擊

                     sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL,

                                                 (LPOVERLAPPED)pAcceptOL);

       以上是一個簡單的示範,如何動态載入一個WinSock2擴充函數,并調用之,其它函數的詳細例子可以看前一篇文章中CGRSMsSockFun類的實作部分。如果使用CGRSMsSockFun 類的話當然更簡單,像下面這樣調用即可:

              CGRSMsSockFun MsSockFun;

              MsSockFun.AcceptEx(skServer, skAccept,pBuf,

                     (LPOVERLAPPED)pAcceptOL);

如果要使用這個類,那麼需要一些修改,主要是異常處理部分,自己注釋掉,或者用其它異常代替掉即可,這個對于有基礎的讀者來說不是什麼難事。

三、定義OVERLAPPED結構:

要想“IOCP”就要自定義OVERLAPPED,這是徹底玩轉IOCP的不二法門,可以這麼說:“江湖上有多少種自定義的OVERLAPPED派生結構體,就有多少種IOCP的封裝!”

OVERLAPPED本身是Windows IOCP機制内部需要的一個結構體,主要用于記錄每個IO操作的“完成狀态”,其内容對于調用者來說是沒有意義的,但是很多時候我們把它當做一個“火車頭”,因為它可以友善的把每個IO操作的相關資料簡單的“從調用處運輸到完成回調函數中”,這是一個非常有用的特性,哪麼如何讓這個火車頭發揮運輸的作用呢?其實很簡單:讓它成為一個自定義的更大結構體的第一個成員。然後用強制類型轉換,将自定義的結構體轉換成OVERLAPPED指針即可。當然不一定非要是新結構體的第一個成員,也可以是任何第n個成員,這時使用VC頭檔案中預定義的一個宏CONTAINING_RECORD再反轉回來即可。

說到這裡一些C++基礎差一點的讀者估計已經很頭暈了,更不知道我再說什麼,那麼我就将好人做到底吧,來解釋下這個來龍去脈。

首先就以我們将要使用的AcceptEx函數為例子看看它的原型吧(知道孫悟空的火眼金睛用來幹嘛的嗎?就是用來看原型的,哈哈哈):

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

);

注意最後一個參數,是一個OVERLAPPED結構體的指針(LP的意思是Long Pointer,即指向32位位址長指針,注意不是“老婆”拼音的縮寫),本身這個參數的意思就是配置設定一塊OVERLAPPED大小的記憶體,在IOCP調用方式下傳遞給AcceptEx函數用,調用者不用去關心裡面的任何内容,而在完成過程中(很多時候是另一個線程中的事情了),通常調用GetQueuedCompletionStatus函數後,會再次得到這個指針,接着讓我們也看看它的原型:

BOOL WINAPI GetQueuedCompletionStatus(

  __in          HANDLE CompletionPort,

  __out         LPDWORD lpNumberOfBytes,

  __out         PULONG_PTR lpCompletionKey,

  __out         LPOVERLAPPED* lpOverlapped,

  __in          DWORD dwMilliseconds

注意這裡的LPOVERLAPPED多了一個*變成了指針的指針,并且前面的說明很清楚Out!很明白了吧,不明白就真的Out了。這裡就可以重新得到調用AcceptEx傳入的LPOVERLAPPED指針,也就是得到了這個“火車頭”,因為隻是一個指針,并沒有詳細的限定能有多大,是以可以在火車頭的後面放很多東西。

再仔細觀察GetQueuedCompletionStatus函數的參數,會發現,這時隻能知道一個IO操作結束了,但是究竟是哪個操作結束了,或者是哪個SOCKET句柄上的操作結束了,并沒有辦法知道。通常這個資訊非常重要,因為隻有在IO操作實際完成之後才能釋放發送或接收等操作的緩沖區。

這些資訊可以定義成如下的一個擴充OVERLAPPED結構:

struct MYOVERLAPPED

{

    OVERLAPPED m_ol;          

    int                    m_iOpType;      

//操作類型 0=AcceptEx 1=DisconnectEx       2=ConnectEx 3=WSARecv等等

    SOCKET          m_skServer;       //服務端SOCKET

    SOCKET         m_skClient;        //用戶端SOCKET

    LPVOID           m_pBuf;            //本次IO操作的緩沖指針

    ......                                           //其它需要的資訊

};

使用時:

MYOVERLAPPED* pMyOL = new MYOVERLAPPED;

ZeroMemory(pMyOL,sizeof(MYOVERLAPPED));

pMyOL-&gt;m_iOpType = 0;        //AcceptEx操作

pMyOL-&gt;m_skServer = skServer;

pMyOL-&gt;m_skClient = skClient;

BYTE* pBuf = new BYTE[256];//一個緩沖

.................. //朝緩沖中寫入東西

pMyOL-&gt;m_pBuf = pBuf;

...............//其它的代碼

AcceptEx(skServer, skClient,pBuf,

0,//将接收緩沖置為0,令AcceptEx直接傳回                     

256,256,NULL,(LPOVERLAPPED)pMyOL));//注意最後這個強制類型轉換

       在完成過程回調線程函數中,這樣使用:

UINT CALLBACK Client_IOCPThread(void* pParam)

       {//IOCP線程函數

              .....................

              DWORD dwBytesTrans = 0;

              DWORD dwPerData = 0;

              LPOVERLAPPED lpOverlapped = NULL;

              while(1)

              {//又見死循環 呵呵呵

                     BOOL bRet = GetQueuedCompletionStatus(

                            pThis-&gt;m_IOCP,&amp;dwBytesTrans,&amp;dwPerData,

                            &amp;lpOverlapped,INFINITE);

                     if( NULL == lpOverlapped )

                     {//沒有真正的完成

                            SleepEx(20,TRUE);//故意置成可警告狀态

                            continue;

                     }

                     //找回“火車頭”以及後面的所有東西

                     MYOVERLAPPED*  pOL = CONTAINING_RECORD(lpOverlapped

, MYOVERLAPPED, m_ol);

                     switch(pOL-&gt;m_iOpType)

case 0: //AcceptEx結束

{//有連結進來了 SOCKET句柄就是 pMyOL-&gt;m_skClient

break;

............................

........................

} //end while

       }//end fun

至此,關于這個“火車頭”如何使用,應該是看明白了,其實就是從函數傳入,又由函數傳回。隻不過其間可能已經轉換了線程環境,是不同的線程了。

這裡再補充一個AcceptEx容易被遺漏的一個細節問題,那就是在AcceptEx完成傳回之後,如下在那個連入的用戶端SOCKET上調用一下:

       int nRet = ::setsockopt(

              pOL-&gt;m_skClient,SOL_SOCKET,SO_UPDATE_ACCEPT_CONTEXT,

              (char *)&amp;pOL-&gt;m_skServer,sizeof(SOCKET));

這樣才可以繼續在這個代表用戶端連接配接的pOL-&gt;m_skClient上繼續調用WSARecv和WSASend。

另外,在AcceptEx完成之後,通常可以用:

LPSOCKADDR addrHost = NULL;      //服務端位址

LPSOCKADDR addrClient = NULL;     //用戶端位址

int lenHost = 0;

int lenClient = 0;

GetAcceptExSockaddrs(

       pOL-&gt;m_pBuf,0,sizeof(sockaddr_in) + 16,sizeof(sockaddr_in) + 16,

       (LPSOCKADDR*) &amp;addrHost,&amp;lenHost,(LPSOCKADDR*) &amp;addrClient,&amp;lenClient);

這樣來得到連入的用戶端位址,以及連入的服務端位址,通常這個位址可以和這個用戶端的SOCKET綁定在一起用map或hash表儲存,友善查詢,就不用再調用那個getpeername得到用戶端的位址了。要注意的是GetAcceptExSockaddrs也是一個WinSock2擴充函數,專門配合AcceptEx使用的,需要像AcceptEx那樣動态載入一下,然後再調用,詳情請見前一篇文章中的CGRSMsSockFun類。

至此AcceptEx算讨論完整了,OVERLAPPED的派生定義也講完了,讓我們繼續下一步。

四、編寫線程池回調函數:

在讨論擴充定義OVERLAPPED結構體時,給出了非線程池版的線程函數的大概架構,也就是傳統IOCP使用的自建線程使用方式,這種方式要自己建立完成端口句柄,自己将SOCKET句柄綁定到完成端口,這裡就不在贅述,主要介紹下調用BindIoCompletionCallback函數時,應如何編寫這個線程池的回調函數,其實它與前面那個線程函數是很類似的。先來看看回調函數長個什麼樣子:

VOID CALLBACK FileIOCompletionRoutine(

  [in]                 DWORD dwErrorCode,

  [in]                 DWORD dwNumberOfBytesTransfered,

  [in]                 LPOVERLAPPED lpOverlapped

第一個參數就是一個錯誤碼,如果是0恭喜你,操作一切ok,如果有錯也不要慌張,前一篇文章中已經介紹了如何翻譯和看懂這個錯誤碼。照着做就是了。

第二個參數就是說這次IO操作一共完成了多少位元組的資料傳輸任務,這個字段有個特殊含義,如果你發現一個Recv操作結束了,并且這個參數為0,那麼就是說,用戶端斷開了連接配接(注意針對的是TCP方式,整個SOCKET池就是為TCP方式設計的)。如果這個情況發生了,在SOCKET池中就該回收這個SOCKET句柄。

第三個參數現在不用多說了,立刻就知道怎麼用它了。跟剛才調用GetQueuedCompletionStatus函數得到的指針是一個含義。

下面就來看一個實作這個回調的例子:

VOID CALLBACK MyIOCPThread(DWORD dwErrorCode

,DWORD dwBytesTrans,LPOVERLAPPED lpOverlapped)

       {//IOCP回調函數

              if( NULL == lpOverlapped )

              {//沒有真正的完成

                     SleepEx(20,TRUE);//故意置成可警告狀态

                     return;

              }

              //找回“火車頭”以及後面的所有東西

              MYOVERLAPPED*  pOL = CONTAINING_RECORD(lpOverlapped

              switch(pOL-&gt;m_iOpType)

看起來很簡單吧?好像少了什麼?對了那個該死的循環,這裡不用了,因為這個是由線程池回調的一個函數而已,線程的活動狀态完全由系統内部控制,隻管認為隻要有IO操作完成了,此函數就會被調用。這裡關注的焦點就完全的放到了完成之後的操作上,而什麼線程啊,完成端口句柄啊什麼的就都不需要了(甚至可以忘記)。

這裡要注意一個問題,正如在《IOCP程式設計之“雙節棍”》中提到的,這個函數執行時間不要過長,否則會出現掉線啊,連接配接不進來啊等等奇怪的事情。

另一個要注意的問題就是,這個函數最好套上結構化異常處理,盡可能的多攔截和處理異常,防止系統線程池的線程因為你糟糕的回調函數而壯烈犧牲,如果加入了并發控制,還要注意防止死鎖,不然你的伺服器會“死”的很難看。

理論上來說,你盡可以把這個函數看做一個與線程池函數等價的函數,隻是他要盡可能的“短”(指執行時間)而緊湊(結構清晰少出錯)。

最後,回調函數定義好了,就可以調用BindIoCompletionCallback函數,将一個SOCKET句柄丢進完成端口的線程池了:

BindIoCompletionCallback((HANDLE)skClient,MyIOCPThread,0);

注意最後一個參數到目前為止,你就傳入0吧。這個函數的神奇就是不見了CreateIoCompletionPort的調用,不見了CreateThread的調用,不見了GetQueuedCompletionStatus等等的調用,省去了n多繁瑣且容易出錯的步驟,一個函數就全部搞定了。

五、服務端調用:

以上的所有步驟在完全了解後,最終讓我們看看SOCKET池如何實作之。

1、按照傳統,要先監聽到某個IP的指定端口上:

SOCKADDR_IN    saServer = {0};

//建立監聽Socket

SOCKET skServer = ::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP

, NULL, 0, WSA_FLAG_OVERLAPPED);

//把監聽SOCKET扔進線程池,這個可以省略               ::BindIoCompletionCallback((HANDLE)skServer,MyIOCPThread, 0);

//必要時打開SO_REUSEADDR屬性,重新綁定到這個監聽位址

BOOL   bReuse=TRUE;                  ::setsockopt(m_skServer,SOL_SOCKET,SO_REUSEADDR

,(LPCSTR)&amp;bReuse,sizeof(BOOL));

saServer.sin_family = AF_INET;

saServer.sin_addr.s_addr = INADDR_ANY;

// INADDR_ANY這個值的魅力是監聽所有本地IP的相同端口

saServer.sin_port = htons(80);      //用80得永生

::bind(skServer,(LPSOCKADDR)&amp;saServer,sizeof(SOCKADDR_IN));

//監聽,隊列長為預設最大連接配接SOMAXCONN

listen(skServer, SOMAXCONN);

2、就是發出一大堆的AcceptEx調用:

for(UINT i = 0; i &lt; 1000; i++)

{//調用1000次

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

skAccept = ::WSASocket(AF_INET,

                                              SOCK_STREAM,

                                              IPPROTO_TCP,

                                              NULL,

                                              0,

                                              WSA_FLAG_OVERLAPPED);

//2011-07-28:以上為原文,下面為改寫後的代碼

skClient = ::WSASocket(AF_INET,SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED );

//丢進線程池中

BindIoCompletionCallback((HANDLE)skAccept ,MyIOCPThread,0);

//2011-07-28:以上為原文寫法,下面為修改後代碼,主要為了更正變量名,友善大家了解

BindIoCompletionCallback((HANDLE)skClient ,MyIOCPThread,0);

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

pMyOL= new MYOVERLAPPED;

ZeroMemory(pBuf,256*sizeof(BYTE));

//發出AcceptEx調用

//注意将AcceptEx函數接收連接配接資料緩沖的大小設定成了0

//這将導緻此函數立即傳回,雖然與不設定成0的方式而言,

//這導緻了一個較低下的效率,但是這樣提高了安全性

//是以這種效率犧牲是必須的

//=================================================================================

//2011-07-28日修改了下面的代碼 把原來的第二個參數skAccept 改為 skClient 為友善大家閱讀和了解

    0,//将接收緩沖置為0,令AcceptEx直接傳回,防止拒絕服務攻擊

    256,256,NULL,(LPOVERLAPPED)pMyOL);

這樣就有1000個AcceptEx在提前等着用戶端的連接配接了,即使1000個并發連接配接也不怕了,當然如果再BT點那麼就放1w個,什麼你要放2w個?那就要看看你的這個IP段的端口還夠不夠了,還有你的系統記憶體夠不夠用。一定要注意同一個IP位址上理論上端口最大值是65535,也就是6w多個,這個要合理的分派,如果并發管理超過6w個以上的連接配接時,怎麼辦呢?那就再插塊網卡租個新的IP,然後再朝那個IP端綁定并監聽即可。因為使用了INADDR_ANY,是以一監聽就是所有本地IP的相同端口,如果伺服器的IP有内外網之分,為了安全和差別起見可以明确指定監聽哪個IP,單IP時就要注意本IP空閑端口的數量問題了。

3、AcceptEx傳回後,也就是線程函數中,判定是AcceptEx操作傳回後,首先需要的調用就是:

GetAcceptExSockaddrs(pBuf,0,sizeof(sockaddr_in) + 16,

       sizeof(sockaddr_in) + 16,(LPSOCKADDR*) &amp;addrHost,&amp;lenHost,

       (LPSOCKADDR*) &amp;addrClient,&amp;lenClient);

int nRet = ::setsockopt(pOL-&gt;m_skClient, SOL_SOCKET,

       SO_UPDATE_ACCEPT_CONTEXT,(char *)&amp;m_skServer,sizeof(m_skServer));

之後就可以WSASend或者WSARecv了。

       4、這些調用完後,就可以在這個m_skClient上收發資料了,如果收發資料結束或者IO錯誤,那麼就回收SOCKET進入SOCKET池:

       DisconnectEx(m_skClient,&amp;pData-&gt;m_ol, TF_REUSE_SOCKET, 0);

       5、當DisconnectEx函數完成操作之後,在回調的線程函數中,像下面這樣重新讓這個SOCKET進入監聽狀态,等待下一個使用者連接配接進來,至此組建SOCKET池的目的就真正達到了:

AcceptEx(skServer, skClient,pBuf , 0,256,256,NULL,

    (LPOVERLAPPED)pMyOL);

//注意在這個SOCKET被重新利用後,後面的再次捆綁到完成端口的操作會傳回一個已設定//的錯誤,這個錯誤直接被忽略即可

         ::BindIoCompletionCallback((HANDLE)skClient,Server_IOCPThread, 0);

       至此服務端的線程池就算搭建完成了,這個SOCKET池也就是圍繞AcceptEx和DisconnectEx展開的,而建立操作就全部都在服務啟動的瞬間完成,一次性投遞一定數量的SOCKET進入SOCKET池即可,這個數量也就是通常所說的最大并發連接配接數,你喜歡多少就設定多少吧,如果連接配接多數量就大些,如果IO操作多,連接配接斷開請求不多就少點,剩下就是調試了。

六、用戶端調用:

1、  主要是圍繞利用ConnectEx開始調用:

SOCKET skConnect = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,

                            NULL,0,WSA_FLAG_OVERLAPPED);

//把SOCKET扔進IOCP

BindIoCompletionCallback((HANDLE)skConnect,MyIOCPThread,0);

//本地随便綁個端口

SOCKADDR_IN LocalIP = {};

LocalIP.sin_family = AF_INET;

LocalIP.sin_addr.s_addr = INADDR_ANY;

LocalIP.sin_port = htons( (short)0 );    //使用0讓系統自動配置設定

int result =::bind(skConnect,(LPSOCKADDR)&amp;LocalIP,sizeof(SOCKADDR_IN));

pMyOL-&gt;m_iOpType = 2;            //ConnectEx操作

pMyOL-&gt;m_skServer = NULL;    //沒有服務端的SOCKET

pMyOL-&gt;m_skClient = skConnect;

ConnectEx(skConnect,(const sockaddr*)pRemoteAddr,sizeof(SOCKADDR_IN),

       NULL,0,NULL,(LPOVERLAPPED)pOL) )

如果高興就可以把上面的過程放到循環裡面去,pRemoteAddr就是遠端伺服器的IP和端口,你可以重複連接配接n多個,然後瘋狂下載下傳東西(别說我告訴你的哈,人家的伺服器當機了找你負責)。注意那個綁定一定要有,不然調用會失敗的。

2、  接下來就線上程函數中判定是ConnectEx操作,通過判定m_iOpType == 2就可以知道,然後這樣做:

setsockopt( pOL-&gt;m_skClient, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT,

                     NULL, 0 );

然後就是自由的按照需要調用WSASend或者WSARecv。

3、  最後使用和服務端相似的邏輯調用DisconnectEx函數,收回SOCKET并直接再次調用ConnectEx連接配接到另一伺服器或相同的同一伺服器即可。

至此用戶端的SOCKET池也搭建完成了,建立SOCKET的工作也是在一開始的一次性就完成了,後面都是利用ConnectEx和DisconnectEx函數不斷的連接配接-收發資料-回收-再連接配接來進行的。用戶端的這個SOCKET池可以用于HTTP下載下傳檔案的用戶端或者FTP下載下傳的服務端(反向服務端)或者用戶端,甚至可以用作一個網遊的機器人系統,也可以作為一個壓力測試的用戶端核心的模型。

七、總結和提高:

以上就是比較完整的如何具體實作SOCKET池的全部内容,因為篇幅的原因就不貼全部的代碼了,我相信各位看客看完之後心中應該有個大概的架構,并且也可以進行實際的代碼編寫工作了。可以用純c來實作也可以用C++來實作。但是這裡要說明一點就是DisconnectEx函數和ConnectEx函數似乎隻能在XP SP2以上和2003Server以上的平台上使用,對于服務端來說這不是什麼問題,但是對于用戶端來說,使用SOCKET池時還要考慮一個相容性問題,不得已還是要放棄在用戶端使用SOCKET池。

SOCKET池的全部精髓就在于提前建立一批SOCKET,然後就是不斷的重複回收再利用,比起傳統的非SOCKET池方式,節省了大量的不斷建立和銷毀SOCKET對象的核心操作,同時借用IOCP函數AcceptEx、ConnectEx和DisconnectEx等的異步IO完成特性提升了整體性能,非常适合用于一些需要大規模TCP連接配接管理的場景,如:HTTP Server FTP Server和遊戲伺服器等。

SOCKET池的本質就是充分的利用了IOCP模型的幾乎所有優勢,是以要用好SOCKET池就要深入的了解IOCP模型,這是前提。有問題請跟帖讨論。