轉自:http://www.vckbase.com/document/viewdoc/?id=1866
英文原文:http://www.codeproject.com/KB/IP/iocp_server_client.aspx
源代碼下載源碼使用了高級的完成端口(IOCP)技術,該技術可以有效地服務于多客戶端。本文提出了一些IOCP編程中出現的實際問題的解決方法,并提供了一個簡單的echo版本的可以傳輸文件的客戶端/服務器程序。
程序截圖:
1.1 環境要求
本文讀者需要熟悉C++、TCP/IP、Socket編程、MFC,和多線程。
源碼使用Winsock 2.0和IOCP技術,要求:
Windows NT/2000或以上:要求Windows NT3.5或以后版本
Windows 95/98/ME:不支持
Visual C++.NET,或完整更新過的Visual C++ 6.0
1.2 摘要
當你開發不同類型的軟件,你遲早必須處理C/S的開發。對一個程序員來說,寫一個通用的C/S編碼是一項困難的工作。本文檔提供了一份簡單但是功能強大的C/S源碼,可以擴展到任何類型的C/S應用程序中。這份源碼使用了高級的IOCP技術,該技術可以高效的服務于多客戶端。IOCP提供了解決“每個客戶端占用一個線程”的瓶頸問題的辦法,只使用幾個處理線程,異步輸入/輸出來發送/接收。IOCP技術被廣泛應用在各種類型的高效服務端,例如Apache等。這份源碼也提供了一系列的在處理通信和C/S軟件中經常使用的功能,如文件接收/傳送功能和邏輯線程池管理。本文重點在于出現在IOCP程序API中實用的解決方案,以及關于源碼的全面的文檔。另外,一份簡單的echo版的可處理多連接和文件傳輸的C/S程序也在這里提供。
2.1 引言
本文提出了一個類,可以用在客戶端和服務端。這個類使用IOCP(Input Output Completion Ports)和異步(非阻塞)機制。…
通過這些簡單的源碼,你可以:
· 服務或連接多客戶端和服務端
· 異步發送或接收文件
· 創建并管理一個邏輯工作者線程池,用以處理繁重的客戶端/服務器請求或計算
找到一份全面但簡單的解決客戶端/服務器通信的源碼是件困難的事情。在網絡上找到的源碼要么太復雜(超過20個類),要命沒有提供足夠的效率。本源碼的設計盡可能簡單,并提供了充足的文檔。在這篇文章中,我們簡潔的呈現出了Winsock API 2.0支持的IOCP技術,說明了在編寫過程中出現的棘手問題,并提出了每一個問題的解決方案。
2.2 異步完成端口介紹
如果一個服務器應用程序不能同時支持多個客戶端,那是毫無意義的,為此,通常使用異步I/O請求和多線程。根據定義,一個異步I/O請求會立即返回,而留下I/O請求處于等待狀態。有時,I/O異步請求的結果必須與主線程同步。這可以通過幾種不同方式解決。同步可以通過下面的方式實現:
> 使用事件 – 當異步請求結束時會馬上觸發一個信號。這種方式的缺點是線程必須檢查并等待事件被觸發
> 使用GetOverlappedResult函數 – 這種方式與上一種方式有相同的缺點。
> 使用Asynchronous Procedure Calls(或APC) – 這種方式有幾個缺點。首先,APC總是在請求線程的上下文中被請求;第二,為了執行APC,請求線程必須在可變等候狀態下掛起。
> 使用IOCP – 這種方式的缺點是必須解決很多實際的棘手的編程問題。編寫IOCP可能有點麻煩。
2.2.1 為什么使用IOCP?
通過使用IOCP,我們可以解決“每個客戶端占用一個線程”的問題。通常普遍認為如果軟件不能運行在真正的多處理器機器上,執行能力會嚴重降低。線程是系統資源,而這些資源既不是無限的,也不是低價的。
IOCP提供了一種方式來使用幾個線程“公平的”處理多客戶端的輸入/輸出。線程被掛起,不占用CPU周期直到有事可做。
2.3 什么是IOCP?
我們已經看到IOCP只是一個線程同步對象,類似于信號燈,因此IOCP并不是一個復雜的概念。一個IOCP對象與幾個支持待定異步I/O請求的I/O對象綁定。一個可以訪問IOCP的線程可以被掛起,直到一個待定的異步I/O請求結束。
3 IOCP是怎樣工作的?
要使用IOCP,你必須處理三件事情,綁定一個socket到完成端口,創建異步I/O請求,并與線程同步。為從異步I/O請求獲得結果,如那個客戶端發出的請求,你必須傳遞兩個參數:CompletionKey參數和OVERLAPPED結構。
3.1 關鍵參數
第一個參數:CompletionKey,是一個DWORD類型的變量。你可以傳遞任何你想傳遞的唯一值,這個值將總是同該對象綁定。正常情況下會傳遞一個指向結構或類的指針,該結構或類包含了一些客戶端的指定對象。在源碼中,傳遞的是一個指向ClientContext的指針。
3.2 OVERLAPPED參數
這個參數通常用來傳遞異步I/O請求使用的內存緩沖。很重要的一點是:該數據將會被鎖定并不允許從物理內存中換出頁面(page out)。
3.3 綁定一個socket到完成端口
一旦創建完成一個完成端口,可以通過調用CreateIoCompletionPort函數來綁定socket到完成端口。形式如下:
BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, m_nIOWorkers);
return h == hCompletionPort;
}
3.4 響應異步I/O請求
響應具體的異步請求,調用函數WSASend和WSARecv。他們也需要一個參數:WSABUF,這個參數包含了一個指向緩沖的指針。一個重要的規則是:通常當服務器/客戶端響應一個I/O操作,不是直接響應,而是提交給完成端口,由I/O工作者線程來執行。這么做的原因是:我們希望公平的分割CPU周期。通過發送狀態給完成端口來發出I/O請求,如下:
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,
pOverlapBuff->GetUsed(),
(DWORD) pContext,
&pOverlapBuff->m_ol);
3.5 與線程同步
與I/O工作者線程同步是通過調用GetQueuedCompletionStatus函數來實現的(如下)。這個函數也提供了CompletionKey參數和OVERLAPPED參數,如下:
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // handle to completion port
LPDWORD lpNumberOfBytes, // bytes transferred
PULONG_PTR lpCompletionKey, // file completion key
LPOVERLAPPED *lpOverlapped, // buffer
DWORD dwMilliseconds // optional timeout value
);
3.6 四個棘手的IOCP編碼問題和解決方法
使用IOCP時會出現一些問題,其中有一些不是很直觀的。在使用IOCP的多線程編程中,一個線程函數的控制流程不是筆直的,因為在線程和通訊直接沒有關系。在這一章節中,我們將描述四個不同的問題,可能在使用IOCP開發客戶端/服務器應用程序時會出現,分別是:
The WSAENOBUFS error problem.(WSAENOBUFS錯誤問題)
The package reordering problem.(包重構問題)
The access violation problem.(訪問非法問題)
3.6.1 WSAENOBUFS問題
這個問題通常很難靠直覺發現,因為當你第一次看見的時候你或許認為是一個內存泄露錯誤。假定已經開發完成了你的完成端口服務器并且運行的一切良好,但是當你對其進行壓力測試的時候突然發現服務器被中止而不處理任何請求了,如果你運氣好的話你會很快發現是因為WSAENOBUFS 錯誤而影響了這一切。
每當我們重疊提交一個send或receive操作的時候,其中指定的發送或接收緩沖區就被鎖定了。當內存緩沖區被鎖定后,將不能從物理內存進行分頁。操作系統有一個鎖定最大數的限制,一旦超過這個鎖定的限制,那么就會產生WSAENOBUFS 錯誤了。
如果一個服務器提交了非常多的重疊的receive在每一個連接上,那么限制會隨著連接數的增長而變化。如果一個服務器能夠預先估計可能會產生的最大并發連接數,服務器可以投遞一個使用零緩沖區的receive在每一個連接上。因為當你提交操作沒有緩沖區時,那么也不會存在內存被鎖定了。使用這種辦法后,當你的receive操作事件完成返回時,該socket底層緩沖區的數據會原封不動的還在其中而沒有被讀取到receive操作的緩沖區來。此時,服務器可以簡單的調用非阻塞式的recv將存在socket緩沖區中的數據全部讀出來,一直到recv返回 WSAEWOULDBLOCK 為止。 這種設計非常適合那些可以犧牲數據吞吐量而換取巨大 并發連接數的服務器。當然,你也需要意識到如何讓客戶端的行為盡量避免對服務器造成影響。在上一個例子中,當一個零緩沖區的receive操作被返回后使 用一個非阻塞的recv去讀取socket緩沖區中的數據,如果服務器此時可預計到將會有爆發的數據流,那么可以考慮此時投遞一個或者多個receive 來取代非阻塞的recv來進行數據接收。(這比你使用1個缺省的8K緩沖區來接收要好的多。)
源碼中提供了一個簡單實用的解決WSAENOBUF錯誤的辦法。我們執行了一個零字節緩沖的異步WSARead(...)(參見 OnZeroByteRead(..))。當這個請求完成,我們知道在TCP/IP棧中有數據,然后我們通過執行幾個有MAXIMUMPACKAGESIZE緩沖的異步WSARead(...)去讀,解決了WSAENOBUFS問題。但是這種解決方法降低了服務器的吞吐量。
總結:
解決方法一:
投遞使用空緩沖區的 receive操作,當操作返回后,使用非阻塞的recv來進行真實數據的讀取。因此在完成端口的每一個連接中需要使用一個循環的操作來不斷的來提交空緩沖區的receive操作。
解決方法二:
在投遞幾個普通含有緩沖區的receive操作后,進接著開始循環投遞一個空緩沖區的receive操作。這樣保證它們按照投遞順序依次返回,這樣我們就總能對被鎖定的內存進行解鎖。
3.6.2 包重構問題
... ... 盡管使用IO完成端口的待發操作將總是按照他們發送的順序來完成,線程調度安排可能使綁定到完成端口的實際工作不按指定的順序來處理。例如,如果你有兩個I/O工作者線程,你可能接收到“字節塊2,字節塊1,字節塊3”。這就意味著:當你通過向I/O完成端口提交請求數據發送數據時,數據實際上用重新排序過的順序發送了。
這可以通過只使用一個工作者線程來解決,并只提交一個I/O請求,等待它完成。但是如果這么做,我們就失去了IOCP的長處。
解決這個問題的一個簡單實用辦法是給我們的緩沖類添加一個順序數字,如果緩沖順序數字是正確的,則處理緩沖中的數據。這意味著:有不正確的數字的緩沖將被存下來以后再用,并且因為執行原因,我們保存緩存到一個HASH MAP對象中(如m_SendBufferMap 和 m_ReadBufferMap)。
獲取這種解決方法的更多信息,請查閱源碼,仔細查看IOCPS類中如下的函數:
GetNextSendBuffer (..) and GetNextReadBuffer(..), to get the ordered send or receive buffer.
IncreaseReadSequenceNumber(..) and IncreaseSendSequenceNumber(..), to increase the sequence numbers.
3.6.3 異步等待讀 和 字節塊包處理問題
最通用的服務端協議是一個基于協議的包,首先X個字節代表包頭,包頭包含了詳細的完整的包的長度。服務端可以讀包頭,計算出需要多少數據,繼續讀取直到讀完一個完整的包。當服務端同時只處理一個異步請求時工作的很好。但是,如果我們想發揮IOCP服務端的全部潛能,我們應該啟用幾個等待的異步讀事件,等待數據到達。這意味著幾個異步讀操作是不按順序完成的,通過等待的讀事件返回的字節塊流將不會按順序處理。而且,一個字節塊流可以包含一個或幾個包,也可能包含部分包,如下圖所示:

這個圖形顯示了部分包(綠色)和完整包(黃色)是怎樣在不同字節塊流中異步到達的。
這意味著我們必須處理字節流來成功的讀取一個完整的包。而且,我們必須處理部分包(圖表中綠色的部分)。這就使得字節流的處理更加困難。這個問題的完整解決方法在IOCPS類的ProcessPackage(…)函數中。
3.6.4 訪問非法問題
這是一個較小的問題,代碼設計導致的問題更勝于IOCP的特定問題。假設一個客戶端連接已經關閉并且一個I/O請求返回一個錯誤標志,然后我們知道客戶端已經關閉。在參數CompletionKey中,我們傳遞了一個指向結構ClientContext的指針,該結構中包含了客戶端的特定數據。如果我們釋放這個ClientContext結構占用的內存,并且同一個客戶端處理的一些其它I/O請求返回了錯誤代碼,我們通過轉換參數CompletionKey為一個指向ClientContext結構的指針并試圖訪問或刪除它,會發生什么呢?一個非法訪問出現了!
這個問題的解決方法是添加一個數字到結構中,包含等待的I/O請求的數量(m_nNumberOfPendingIO),然后當我們知道沒有等待的I/O請求時刪除這個結構。這個功能通過函數EnterIoLoop(…) 和ReleaseClientContext(…)來實現。
3.7 源碼略讀
源碼的目標是提供一系列簡單的類來處理所有IOCP編碼中的問題。源碼也提供了一系列通信和C/S軟件中經常使用的函數,如文件接收/傳送函數,邏輯線程池處理,等等。

上圖功能性的圖解說明了IOCP類源碼。
我們有幾個IO工作者線程通過完成端口來處理異步IO請求,這些工作者線程調用一些虛函數,這些虛函數可以把需要大量計算的請求放到一個工作隊列中。邏輯工作者通過類中提供的這些函數從隊列中取出任務、處理并發回結果。GUI經常與主類通信,通過Windows消息(因為MFC不是線程安全的)、通過調用函數或通過使用共享的變量。
圖三
上圖顯示了類結構縱覽。
圖3中的類說明如下:
> CIOCPBuffer:管理異步請求的緩存的類。
> IOCPS:處理所有通信的主類。
> JobItem:保存邏輯工作者線程要處理的任務的結構。
> ClientContex:保存客戶端特定信息的結構(如狀態、數據,等等)。
3.7.1 緩沖設計 - CIOCPBuffer類
使用異步I/O調用時,我們必須提供私有的緩沖區供I/O操作使用。
當我們將帳號信息放入分配的緩沖供使用時有許多情況需要考慮:
.分配和釋放內存代價高,因此我們應重復使用以及分配的緩沖(內存),
因此我們將緩沖保存在列表結構中,如下所示:
// Free Buffer List..
CCriticalSection m_FreeBufferListLock;
CPtrList m_FreeBufferList;
// OccupiedBuffer List.. (Buffers that is currently used)
CCriticalSection m_BufferListLock;
CPtrList m_BufferList;
// Now we use the function AllocateBuffer(..)
// to allocate memory or reuse a buffer.
.有時,當異步I/O調用完成后,緩沖里可能不是完整的包,因此我們需要分割緩沖去取得完整的信息。在CIOCPS類中提供了SplitBuffer函數。
同樣,有時候我們需要在緩沖間拷貝信息,IOCPS類提供了AddAndFlush函數。
. 眾所周知,我們也需要添加序號和狀態(IOType 變量, IOZeroReadCompleted, 等等)到我們的緩沖中。
. 我們也需要有將數據轉換到字節流或將字節流轉換到數據的方法,CIOCPBuffer也提供了這些函數。
以上所有問題都在CIOCPBuffer中解決。
3.8 如何使用源代碼
從IOCP繼承你自己的類(如圖3),實現IOCPS類中的虛函數(例如,threadpool),
在任何類型的服務端或客戶端中實現使用少量的線程有效地管理大量的連接。
3.8.1 啟動和關閉服務端/客戶端
調用下面的函數啟動服務端
BOOL Start(int nPort=999,int iMaxNumConnections=1201,
int iMaxIOWorkers=1,int nOfWorkers=1,
int iMaxNumberOfFreeBuffer=0,
int iMaxNumberOfFreeContext=0,
BOOL bOrderedSend=TRUE,
BOOL bOrderedRead=TRUE,
int iNumberOfPendlingReads=4);
nPort
服務端偵聽的端口. ( -1 客戶端模式.)
iMaxNumConnections
允許最大的連接數. (使用較大的數.)
iMaxIOWorkers
I/O工作線程數
nOfWorkers
邏輯工作者數量Number of logical workers. (可以在運行時改變.)
iMaxNumberOfFreeBuffer
重復使用的緩沖最大數. (-1 不使用, 0= 不限)
iMaxNumberOfFreeContext
重復使用的客戶端信息對象數 (-1 for 不使用, 0= 不限)
bOrderedRead
順序讀取. (我們已經在 3.6.2. 處討論過)
bOrderedSend
順序寫入. (我們已經在 3.6.2. 處討論過)
iNumberOfPendlingReads
等待讀取數據時未決的異步讀取循環數
連接到遠程服務器(客戶端模式nPort=-1),調用函數:
CodeConnect(const CString &strIPAddr, int nPort)
.strIPAddr
遠程服務器的IP地址
.nPort
端口
調用ShutDown()關閉連接
例如:
if(!m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("Error could not start the Client");
….
m_iocp.ShutDown();
4.1 源代碼描述
更多關于源代碼的信息請參考代碼里的注釋。
4.1.1 虛函數
NotifyNewConnection
新的連接已接受
NotifyNewClientContext
空的ClientContext結構被分配
NotifyDisconnectedClient
客戶端連接斷開
ProcessJob
邏輯工作者需要處理一個工作
NotifyReceivedPackage
新的包到達
NotifyFileCompleted
文件傳送完成。
4.1.2 重要變量
所有變量共享使用時必須加鎖避免存取違例,所有需要加鎖的變量,名稱為XXX則鎖變量名稱為XXXLock。
m_ContextMapLock;
保存所有客戶端數據(socket,客戶端數據,等等)
ContextMap m_ContextMap;
m_NumberOfActiveConnections
保存已連接的連接數
4.1.3 重要函數
GetNumberOfConnections()
返回連接數
CString GetHostAdress(ClientContext* p)
提供客戶端上下文,返回主機地址
BOOL ASendToAll(CIOCPBuffer *pBuff);
發送緩沖上下文到所有連接的客戶端
DisconnectClient(CString sID)
根據客戶端唯一編號,斷開指定的客戶端
CString GetHostIP()
返回本地IP
JobItem* GetJob()
將JobItem從隊列中移出, 如果沒有job,返回 NULL
BOOL AddJob(JobItem *pJob)
添加Job到隊列
BOOL SetWorkers(int nThreads)
設置可以任何時候調用的邏輯工作者數量
DisconnectAll();
斷開所有客戶端
ARead(…)
異步讀取
ASend(…)
異步發送,發送數據到客戶端
ClientContext* FindClient(CString strClient)
根據字符串ID尋找客戶(非線程安全)
DisconnectClient(ClientContext* pContext, BOOL bGraceful=FALSE);
端口客戶
DisconnectAll()
端口所有客戶
StartSendFile(ClientContext *pContext)
根據ClientContext結構發送文件(使用經優化的transmitfile(..) 函數)
PrepareReceiveFile(..)
接收文件準備。調用該函數時,所有進入的字節流已被寫入到文件。
PrepareSendFile(..)
打開文件并發送包含文件信息的數據包。函數禁用ASend(..)函數,直到文件傳送關閉或中斷。
DisableSendFile(..)
禁止發送文件模式
DisableRecevideFile(..)
禁止文件接收模式
5.1 文件傳輸
文件傳輸使用Winsock 2.0 中的TransmitFile函數。TransmitFile函數通過連接的socket句柄傳送文件數據。函數使用操作系統的高速緩沖管理器(cache manager)接收文件數據,通過sockets提供高性能的文件數據傳輸。異步文件傳輸要點:
在TransmitFile函數返回前,所有其他發送或寫入到該socket的操作都將無法執行,因為這將使文件數據混亂。
因此,在PrepareSendFile()函數調用之后,所有ASend都被禁止。
因為操作系統連續讀取文件數據,你可以使用FILE_FLAG_SEQUENTIAL_SCAN參數來優化高速緩沖性能。
發送文件時我們使用了內核異步操作(TF_USE_KERNEL_APC)。TF_USE_KERNEL_APC的使用可以更好地提升性能。有可能, 無論如何,TransmitFile在線程中的大量使用,這種情形可能會阻止APCs的調用.
文件傳輸按如下順序執行:服務器調用PrepareSendFile(..)函數初始化文件傳輸。客戶端接收文件信息時,調用PrepareReceiveFile(..)作接收前的準備,并發送一個包到服務器告知開始文件傳送。當包到達服務器端,服務器端調用StartSendFile(..)采用高性能的TransmitFile函數發送指定文件。
6 源代碼示例
提供的源代碼演示代碼是一個echo客戶端/服務器端程序,并提供了對文件傳輸的支持(圖4)。在代碼中,MyIOCP類從IOCP繼承,處理客戶端/服務器端的通訊,所涉及的虛函數可以參見4.1.1處。
客戶端或服務器端最重要的部分是虛函數NotifyReceivedPackage,定義如下:
void MyIOCP::NotifyReceivedPackage(CIOCPBuffer *pOverlapBuff,
int nSize,ClientContext *pContext)
{
BYTE PackageType=pOverlapBuff->GetPackageType();
switch (PackageType)
{
case Job_SendText2Client :
Packagetext(pOverlapBuff,nSize,pContext);
break;
case Job_SendFileInfo :
PackageFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_StartFileTransfer:
PackageStartFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_AbortFileTransfer:
DisableSendFile(pContext);
break;
};
}
函數接收進入的信息并執行遠程連接發送的請求。這種情況,只是簡單的echo或文件傳輸的情形。服務器端和客戶端源代碼分成兩個工程,IOCP和IOCPClient。
6.1 編譯問題
使用VC++6.0或VC.NET編譯,你可能在編譯CFile時得到一些奇怪的錯誤,如:
“if (pContext->m_File.m_hFile !=
INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "
"from 'void *' to 'unsigned int'”
這個問題可以通過更新頭文件(*.h)或VC++ 6.0的版本或改變類型轉換錯誤來解決,在修正了錯誤后,服務器端/客戶端源代碼可以在不需MFC的情況下使用。
7 特別的考慮和經驗總結
當你在其他類型的程序中使用本代碼,有一些可以避免的編程陷阱和多線程問題。
非確定錯誤指的是那些隨機出現的錯誤,很難通過執行相同順序的任務來重現這些錯誤。
這是最壞的錯誤類型,通常,錯誤出現在內部源代碼的設計中。當服務器端有多個IO工作線程在運行,
為客戶端提供連接,如果程序員沒有考慮多線程環境,可能會發生存取違例等不確定錯誤。
經驗 #1:
在未對上下文加鎖時,不要讀寫客戶上下文(例如:ClientContext)。
通知函數(例如:Notify*(ClientContext *pContext))已經是線程安全,處理成員變量ClientContext可以不需要解鎖、解鎖。
//不要這樣使用
// …
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
// …
// 應該這樣使用
//….
pContext->m_ContextLock.Lock();
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
pContext->m_ContextLock.Unlock();
//…
大家都知道的,當你鎖定上下文,其他線程或GUI都將等待它。
經驗#2:
避免使用復雜的和其他類型的"上下文鎖",應為容易造成死鎖(例如:A在等待B,B在等待C,C在等待A,A死鎖)
pContext-> m_ContextLock.Lock();
//… code code ..
pContext2-> m_ContextLock.Lock();
// code code..
pContext2-> m_ContextLock.Unlock();
// code code..
pContext-> m_ContextLock.Unlock();
以上代碼可能導致死鎖
經驗 #3:
不要在通知函數(例如:Notify*(ClientContext *pContext))以外處理客戶上下文,如果你需要這樣做,你
要放入
m_ContextMapLock.Lock();
…
m_ContextMapLock.Unlock();
參考如下代碼:
ClientContext* pContext=NULL ;
m_ContextMapLock.Lock();
pContext = FindClient(ClientID);
// safe to access pContext, if it is not NULL
// and are Locked (Rule of thumbs#1:)
//code .. code..
m_ContextMapLock.Unlock();
// Here pContext can suddenly disappear because of disconnect.
// do not access pContext members here.
8 將來的工作
將來,代碼將提供以下功能:
添加支持AcceptEx(..)接受新連接,處理短連接和DOS攻擊。
源代碼兼容Win32,STL,WTL等環境。