一、CAsyncSocket異步機制
當你獲得了一個異步連接配接後,實際上你掃除了發送動作與接收動作之間的依賴性。是以你随時可以發包,也随時可能收到包。發送、接收函數都是異步非阻塞的,頃刻就能完成,是以收發交錯進行着,你可以一直工作,保持很高的效率。但是,正因為發送、接收函數都是異步非阻塞的,是以僅調用它們并不能保障發送或接收的完成。例如發送函數Send,調用它可能有3種結果:錯誤、部分完成、全部完成。其中錯誤又分兩種情況:一種是由各種網絡問題導緻的失敗,你需要馬上決定是放棄本次操作,還是啟用某種對策;另一種是“忙”,你實際上不用馬上理睬。你需要調用GetLastError來判斷是哪種情況,GetLastError傳回WSAEWOULDBLOCK,代表“忙”,為什麼當你Send得到WSAEWOULDBLOCK卻不用理睬呢?因為CAsyncSocket會記得你的SendWSAEWOULDBLOCK了,待發送的資料會寫入CAsyncSocket内部的發送緩沖區,并會在不忙的時候自動調用OnSend,發送内部緩沖區裡的資料。同樣,如果Send隻完成了一部分,你也不需要理睬,尚未發送的資料同樣會寫入CAsyncSocket内部的發送緩沖區,并在适當的時候自動調用OnSend完成發送。與OnSend協助Send完成工作一樣,OnRecieve、OnConnect、OnAccept也會分别協助Recieve、Connect、Accept完成工作。這一切都通過消息機制完成:在你使用CAsyncSocket之前,必須調用AfxSocketInit初始化WinSock環境,而AfxSocketInit會建立一個隐藏的CSocketWnd對象,由于這個對象由Cwnd派生,是以它能夠接收Windows消息。一方面它會接受各個CAsyncSocket的狀态報告,另一方面它能捕捉系統發出的各種SOCKET事件。是以它能夠成為高層CAsyncSocket對象與WinSock底層之間的橋梁:例如某CAsyncSocket在Send時WSAEWOULDBLOCK了,它就會發送一條消息給CSocketWnd作為報告,CSocketWnd會維護一個報告登記表,當它收到底層WinSock發出的空閑消息時,就會檢索報告登記表,然後直接調用報告者的OnSend函數。是以前文所說的CAsyncSocket會自動調用OnXxx,實際上是不對的,真正的調用者是CSocketWnd——它是一個CWnd對象,運作在獨立的線程中。
二、CAsyncSocket的用法
你會發現直接使用CAsyncSocket類來建立對象很難滿足業務需要。因為CAsyncSocket隐藏了很多細節并不告訴你,你對業務的控制能力得不到保障。例如你需要知道每筆訂單發給對端業務系統的确切時間,如果你直接使用CAsyncSocket的話,你無從得知,因為每次隻要Send函數不是馬上全部完成,而是進入了緩沖區等待CSocketWnd調用OnSend,這個訂單到底是啥時候發出去的就很難說了。OnSend内部畢竟還是調用Send來發送的,是以可能OnSend一次也并不能完全發送,還要等待下一次甚至多次OnSend。而且這筆糊塗賬全悶在CSocketWnd肚子裡,它并不提供一個讓你了解的機會。接收就更成問題了,通常業務系統會定義多種類型的資料包格式,你既不知道下一個将收到的包是哪種包,也不知道它将何時抵達。是以你在業務邏輯上永遠不應該主動調用Recieve,而應該被動等待資料抵達時OnRecieve被自動調用,但OnRecieve也不懂你的業務包的格式,甚至也不會向你報告請示。CAsyncSocket隻能處理位元組流,但你需要處理的通常是業務流。是以,使用CAsyncSocket,通常是需要派生的,一般你至少需要重載OnSend和OnRecieve,你需要參與其中以進行業務控制。當然,既然你重載了OnSend和OnRecieve,你得自己編碼實作協助Send、Recieve保證完成收發,你可能發現你自己并不能通路CAsyncSocket的内部緩沖區,是以你還要自己定義收發緩沖區并記錄收發進度。其實你不大可能在OnSend或OnRecieve裡面做好一切,你往往不得不
與你的主線程互動,你可以參考以下兩種模式:第一種,在你的CXxxSocket類中申明一個你的主線程類指針,作為成員變量,典型的情況下,主線程是一個CXxxDlg,是以:
//CXxxSocket類頭檔案
Class CXxxDlg;
Class CXxxSocket : public CAsyncSocket
{
CXxxDlg* m_pDlg;
...
virtual void OnReceive(int nErrorCode);
}
//CXxxSocket類實作檔案
...
void CXxxSocket::OnReceive(int nErrorCode)
{
...
m_pDlg->SomeFunc();
...
}
...
//CXxxDlg類實作檔案
...
CXxxSocket sock;
sock.m_pDlg=this;
sock.Create(...);
...
這種方法很友善,但并不靈活,代碼可重用性低,隻有CXxxDlg類能夠使用CXxxSocket類。
第二種,在你的CXxxSocket類中申明一個視窗句柄作為成員變量,當你需要主線程參與工作時,向主線程發送自定義消息:
//CXxxSocket類頭檔案
static UINT WM_SOCKET_MSG=::RegisterWindowMessage("SocketMsg");
Class CXxxSocket : public CAsyncSocket
{
HWND m_hWnd;
...
virtual void OnReceive(int nErrorCode);
}
//CXxxSocket類實作檔案
...
void CXxxSocket::OnReceive(int nErrorCode)
{
...
SendMessage(m_hWnd,WM_SOCKET_MSG,...);
...
}
...
//CXxxDlg類實作檔案
...
CXxxSocket sock;
sock.m_hWnd=GetSafeHwnd();
sock.Create(...);
...
三、CAsyncSocket的運作流程
使用CAsyncSocket時,Send流程和Recieve流程是不同的,不了解這一點就不可能順利使用CAsyncSocket。MSDN對CAsyncSocket的解釋很容易讓你了解為:隻有OnSend被觸發時你Send才有意義,你才應該Send,同樣隻有OnRecieve被觸發時你才應該Recieve。很不幸,你錯了:你會發現,連接配接建立的同時,OnSend就第一次被觸發了,嗯,這很好,但你現在還不想Send,你讓OnSend傳回,幹點其他的事情,等待下一次OnSend試試看?實際上,你再也等不到OnSend被觸發了。因為,除了第一次以外,OnSend的任何一次觸發,都源于你調用了Send,但碰到了WSAEWOULDBLOCK或隻完成了部分發送!是以,使用CAsyncSocket時,針對發送的流程邏輯應該是:你需兩個
成員變量,一個發送任務表,一個記錄發送進度。你可以,也應該,在任何你需要的時候,主動調用Send來發送資料,同時更新任務表和發送進度。而OnSend,則是你的負責擦屁股工作的助手,它被觸發時要幹的事情就是根據任務表和發送進度調用Send繼續發,若此次發送沒能将任務表全部發送完成,根據發送結果更新發送進度;若任務表
已全部發送完畢,則清空任務表及發送進度。使用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);