MFC對SOCKET程式設計的支援其實是很充分的,然而其文檔是語焉不詳的。以至于大多數用VC編寫的功能稍複雜的網絡程式,還是使用API的。故CAsyncSocket及CSocket事實上成為疑難,群衆多敬而遠之。餘好事者也,不忍資源浪費,特為之注解。
一、CAsyncSocket與CSocket的差別
前者是異步通信,後者是同步通信;前者是非阻塞模式,後者是阻塞模式。另外,異步非阻塞模式有時也被稱為長連接配接,同步阻塞模式則被稱為短連接配接。為了更明白地講清楚兩者的差別,舉個例子:設想你是一位體育老師,需要測驗100位同學的400米成績。你當然不會讓100位同學一起起跑,因為當同學們傳回終點時,你根本來不及掐表記錄各位同學的成績。
如果你每次讓一位同學起跑并等待他回到終點你記下成績後再讓下一位起跑,直到所有同學都跑完。恭喜你,你已經掌握了同步阻塞模式。
你設計了一個函數,傳入參數是學生号和起跑時間,傳回值是到達終點的時間。你調用該函數100次,就能完成這次測驗任務。這個函數是同步的,因為隻要你調用它,就能得到結果;這個函數也是阻塞的,因為你一旦調用它,就必須等待,直到它給你結果,不能去幹其他事情。
如果你一邊每隔10秒讓一位同學起跑,直到所有同學出發完畢;另一邊每有一個同學回到終點就記錄成績,直到所有同學都跑完。恭喜你,你已經掌握了異步非阻塞模式。
你設計了兩個函數,其中一個函數記錄起跑時間和學生号,該函數你會主動調用100次;另一個函數記錄到達時間和學生号,該函數是一個事件驅動的callback函數,當有同學到達終點時,你會被動調用。你主動調用的函數是異步的,因為你調用它,它并不會告訴你結果;這個函數也是非阻塞的,因為你一旦調用它,它就馬上傳回,你不用等待就可以再次調用它。但僅僅将這個函數調用100次,你并沒有完成你的測驗任務,你還需要被動等待調用另一個函數100次。
當然,你馬上就會意識到,同步阻塞模式的效率明顯低于異步非阻塞模式。那麼,誰還會使用同步阻塞模式呢?
不錯,異步模式效率高,但更麻煩,你一邊要記錄起跑同學的資料,一邊要記錄到達同學的資料,而且同學們回到終點的次序與起跑的次序并不相同,是以你還要不停地在你的成績冊上查找學生号。忙亂之中你往往會張冠李戴。
你可能會想出更聰明的辦法:你帶了很多塊秒表,讓同學們分組互相測驗。恭喜你!你已經掌握了多線程同步模式!每個拿秒表的同學都可以獨立調用你的同步函數,這樣既不容易出錯,效率也大大提高,隻要秒表足夠多,同步的效率也能達到甚至超過異步。
可以了解,你現的問題可能是:既然多線程同步既快又好,異步模式還有存在的必要嗎?
很遺憾,異步模式依然非常重要,因為在很多情況下,你拿不出很多秒表。你需要通信的對端系統可能隻允許你建立一個SOCKET連接配接,很多金融、電信行業的大型業務系統都如此要求。
現在,你應該已經明白了:CAsyncSocket用于在少量連接配接時,處理大批量無步驟依賴性的業務。CSocket用于處理步驟依賴性業務,或在可多連接配接時配合多線程使用。
二、CAsyncSocket異步機制
當你獲得了一個異步連接配接後,實際上你掃除了發送動作與接收動作之間的依賴性。是以你随時可以發包,也随時可能收到包。發送、接收函數都是異步非阻塞的,頃刻就能傳回,是以收發交錯進行着,你可以一直工,保持很高的效率。但是,正因為發送、接收函數都是異步非阻塞的,是以僅調用它們并不能保障發送或接收的完成。例如發送函數Send,調用它可能有4種結果:
1、錯誤,Send()==SOCKET_ERROR,GetLastError()!=WSAEWOULDBLOCK,這種情況可能由各種網絡問題導緻,你需要馬上決定是放棄本次操作,還是啟用某種對策。
2、忙,Send()==SOCKET_ERROR,GetLastError()==WSAEWOULDBLOCK,導緻這種情況的原因是,你的發送緩沖區已被填滿或對方的接受緩沖區已被填滿。這種情況你實際上不用馬上理睬。因為AsyncSocket會記得你的Send WSAEWOULDBLOCK了,待發送的資料會寫入CAsyncSocket内部的發送緩沖區,并會在不忙的時候自動調用OnSend,發送内部緩沖區裡的資料。
3、部分完成,0<Send(pBuf,nLen)<nLen,導緻這種情況的原因是,你的發送緩沖區或對方的接收緩沖區中剩餘的空位不足以容納你這次需要發送的全部資料。 處理這種情況的通常做法是繼續發送尚未發送的資料直到全部完成或WSAEWOULDBLOCK。這種情況很容易讓人産生疑惑,既然緩沖區空位不足,那麼本次發送就已經填滿了緩沖區,幹嘛還要繼續發送呢,就像WSAEWOULDBLOCK了一樣直接交給OnSend去處理剩餘資料的發送不是更合理嗎?然而很遺憾,CAsyncSocket不會記得你隻完成了部分發送任務進而在合适的時候觸發OnSend,因為你并沒有WSAEWOULDBLOCK。你可能認為既然已經填滿緩沖區,繼續發送必然會WSAEWOULDBLOCK,其實不然,假如WSAEWOULDBLOCK是由于對方讀取接收緩沖區不及時引起的,繼續發送的确很可能會WSAEWOULDBLOCK,但假如WSAEWOULDBLOCK是由于發送緩沖區被填滿,就不一定了,因為你的網卡處理發送緩沖區中資料的速度不見得比你往發送緩沖區拷貝資料的速度更慢,這要取決與你競争CPU、記憶體、帶寬資源的其他應用程式的具體情況。假如這時候CPU負載較大而網卡負載較低,則雖然剛剛發送緩沖區是滿的,你繼續發送也不會WSAEWOULDBLOCK。
4、完成,Send(pBuf,nLen)==nLen與OnSend協助Send完成工作一樣,OnRecieve、OnConnect、OnAccept也會分别協助Recieve、Connect、Accept完成工作。這一切都通過消息機制完成:
在你使用CAsyncSocket之前,必須調用AfxSocketInit初始化WinSock環境,而AfxSocketInit會建立一個隐藏的CSocketWnd對象,由于這個對象由Cwnd派生,是以它能夠接收Windows消息。是以它能夠成為高層CAsyncSocket對象與WinSock底層之間的橋梁。例如某CAsyncSocket在Send時WSAEWOULDBLOC了,它就會發送一條消息給CSocketWnd作為報告,CSocketWnd會維護一個報告登記表,當它收到底層inSock發出的空閑消息時,就會檢索報告登記表,然後直接調用報告者的OnSend函數。是以前文所說的CAsyncSocket會自動調用OnXxx,實際上是不對的,真正的調用者是CSocketWnd——它是一個CWnd對象,運作在獨立的線程中。
使用CAsyncSocket時,Send流程和Recieve流程是不同的,不了解這一點就不可能順利使用CAsyncSocket。MSDN對CAsyncSocket的解釋很容易讓你了解為:隻有OnSend被觸發時你Send才有意義,你才應該Send,同樣隻有OnRecieve被觸發時你才應該Recieve。很不幸,你錯了:
你會發現,連接配接建立的同時,OnSend就第一次被觸發了,嗯,這很好,但你現在還不想Send,你讓OnSend傳回,幹點其他的事情,等待下一次OnSend試試看?實際上,你再也等不到OnSend被觸發了。因為,除了第一次以外,OnSend的任何一次觸發,都源于你調用了Send,但碰到了WSAEWOULDBLOCK!
是以,使用CAsyncSocket時,針對發送的流程邏輯應該是:你需兩個成員變量,一個發送任務表,一個記錄發送進度。你可以,也應該,在任何你需要的時候,主動調用Send來發送資料,同時更新任務表和發送進度。而OnSend,則是你的負責擦屁股工作的助手,它被觸發時要幹的事情就是根據任務表和發送進度調用Send繼續發。若又沒能将任務表全部發送完成,更新發送進度,退出,等待下一次OnSend;若任務表已全部發送完畢,則清空任務表及發送進度。
使用CAsyncSocket的接收流程邏輯是不同的:你永遠不需要主動調用Recieve,你隻應該在OnRecieve 中等待。由于你不可能知道将要抵達的資料類型及次序,是以你需要定義一個已收資料表作為成員變量來存儲已收到但尚未處理的資料。每次OnRecieve被觸發,你隻需要被動調用一次Recieve來接受固定長度的資料,并添加到你的已收資料表後。然後你需要掃描已收資料表,若其中已包含一條或數條完整的可解析的業務資料包,截取出來,調用業務處理視窗的處理函數來處理或作為消息參數發送給業務處理視窗。而已收資料表中剩下的資料,将等待下次OnRecieve中被再次組合、掃描并處理。
在長連接配接應用中,連接配接可能因為各種原因中斷,是以你需要自動重連。你需要根據 CAsyncSocket的成員變量m_hSocket來判斷目前連接配接狀态:if(m_hSocket==INVALID_SOCKET)。當然,很奇怪的是,即使連接配接已經中斷,OnClose也已經被觸發,你還是需要在OnClose中主動調用Close,否則m_hSocket并不會被自動指派為INVALID_SOCKET。
在很多長連接配接應用中,除建立連接配接以外,還需要先Login,然後才能進行業務處理,連接配接并Login是一個步驟依賴性過程,用異步方式處理反而會很麻煩,而CAsyncSocket是支援切換為同步模式的,你應該掌握在适當的時候切換同異步模式的方法:
DWORD dw;
//切換為同步模式
dw=0;
IOCtl(FIONBIO,&dw);
...
//切換回異步模式
dw=1;
IOCtl(FIONBIO,&dw);
三、CSocket的用法
CSocket在CAsyncSocket的基礎上,修改了Send、Recieve等成員函數,幫你内置了一個用以輪詢收發緩沖區的循環,變成了同步短連接配接模式。短連接配接應用簡單明了,CSocket經常不用派生就可以直接使用,但也有些問題:
1、用作監聽的時候
曾經看到有人自己建立線程,線上程中建立CSocket對象進行Listen、Accept,若Accept成功則再起一個線程繼續Listen、Accept。可以說他完全不了解CSocket,實際上CSocket的監聽機制已經内置了多線程機制,你隻需要從CSocket派生,然後重載OnAccept:
//CListenSocket頭檔案
class CListenSocket : public CSocket
{
public:
CListenSocket(HWND hWnd=NULL);
HWND m_hWnd; //事件處理視窗
virtual void OnAccept(int nErrorCode);
};
//CListenSocket實作檔案
#include "ListenSocket.h"
CListenSocket::CListenSocket(HWND hWnd){m_hWnd=hWnd;}
void CListenSocket::OnAccept(int nErrorCode)
{
SendMessage(m_hWnd,WM_SOCKET_MSG,SOCKET_CLNT_ACCEPT,0);
CSocket::OnAccept(nErrorCode);
}
//主線程
...
m_pListenSocket=new CListenSocket(m_hWnd);
m_pListenSocket->Create(...);
m_pListenSocket->Listen();
...
LRESULT CXxxDlg::OnSocketMsg(WPARAM wParam, LPARAM lParam)
{
UINT type=(UINT)wParam;
switch(type)
{
case SOCKET_CLNT_ACCEPT:
{
CSocket* pSocket=new CSocket;
if(!m_pListenSocket->Accept(*pSocket))
{
delete pSocket;
break;
}
...
}
...
}
}
2、用于多線程的時候
常看到人說CSocket在子線程中不能用,其實不然。實際情況是:
直接使用CSocket動态建立的對象,将其指針作為參數傳遞給子線程,則子線程中進行收發等各種操作都沒問題。但如果是使用CSocket派生類建立的對象,就要看你重載了哪些方法,假如你僅重載了OnClose,則子線程中你也可以正常收發,但不能Close!
因為CSocket是用内部循環做到同步的,并不依賴各OnXxx,它不需要與CSocketWnd互動。但當你派生并重載OnXxx後,它為了提供消息機制就必須與CSocketWnd互動。當你調用AfxSocketInit時,你的主線程會獲得一個通路CSocketWnd的句柄,對CSocketWnd的通路是MFC自動幫你完成的,是被隐藏的。而你自己建立的子線程并不自動具備通路CSocketWnd的機制,是以子線程中需要通路CSocketWnd的操作都會失敗。
常看到的解決辦法是給子線程傳遞SOCKET句柄而不是 CSocket對象指針,然後在子線程中建立CSocket臨時對象并Attach傳入的句柄,用完後再Dettach并delete臨時對象。俺沒有這麼幹過,估計是因為Attach方法含有擷取CSocketWnd句柄的内置功能。
俺的解決方案還是使用自定義消息,比如俺不能在子線程中Close,那麼,俺可以給主線程發送一條消息,讓主線程的消息處理函數來完成Close,也很友善。
CSocket一般配合多線程使用,隻要你想收發資料,你就可以建立一個CSocket對象,并建立一個子線程來進行收發。是以被阻塞的隻是子線程,而主線程總是可以随時建立子線程去幫它幹活。由于可能同時有很多個CSocket對象在工作,是以你一般還要建立一個清單來儲存這些CSocket對象的辨別,這樣你可能通過在清單中檢索辨別來區分各個CSocket對象,當然,由于記憶體位址的唯一性,對象指針本身就可以作為辨別。
相對CAsyncSocket而言,CSocket的運作流程更直覺也更簡單
來自: http://hi.baidu.com/andywangcn/blog/item/b30e6d1f939d781241341786.html