一長篇論述CSocket,CAsyncSocket 的論壇文章(轉)

lynon發表於2018-04-25

轉自:

http://tieba.baidu.com/p/744710593?pid=9115063767&cid=0


MFC疑難註解:CAsyncSocket及CSocket

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,導致這種情況的原因是,你的傳送


2018-04-25 16:53 廣告
緩衝區已被填滿或對方的接受緩衝區已被填滿。這種情況你實際上不用馬上理睬。因為CAsyncSocket會
記得你的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時WSAEWOULDBLOCK了,它就會
傳送一條訊息給CSocketWnd作為報告,CSocketWnd會維護一個報告登記表,當它收到底層WinSock發出的
空閒訊息時,就會檢索報告登記表,然後直接呼叫報告者的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的運作流程更直觀也更簡單,至於CSocketFile、CArchive之類的,似乎
也不需要多說什麼,就這樣結束吧。 

轉自:創意安天論壇


好多字...


好東西,MARK


相關文章