Socket同步 異步通訊

MFC對SOCKET編程的支持實際上是很充分的,然而其文檔是語焉不詳的。以致於大多數用VC編寫的功能稍
複雜的網絡程序,仍是使用API的。故CAsyncSocket及CSocket事實上成爲疑難,羣衆多敬而遠之。餘
好事者也,不忍資源浪費,特爲之註解。

1、CAsyncSocket與CSocket的區別

前者是異步通訊,後者是同步通訊;前者是非阻塞模式,後者是阻塞模式。另外,異步非阻塞模式有
時也被稱爲長鏈接,同步阻塞模式則被稱爲短鏈接。爲了更明白地講清楚二者的區別,舉個例子:

設想你是一位體育老師,須要測驗100位同窗的400米成績。你固然不會讓100位同窗一塊兒起跑,由於當
同窗們返回終點時,你根原本不及掐表記錄各位同窗的成績。

若是你每次讓一位同窗起跑並等待他回到終點你記下成績後再讓下一位起跑,直到全部同窗都跑完。恭
喜你,你已經掌握了同步阻塞模式。

你設計了一個函數,傳入參數是學生號和起跑時間,返回值是到達終點的時間。你調用該函數100次,
就能完成此次測驗任務。這個函數是同步的,由於只要你調用它,就能獲得結果;這個函數也是阻塞的,
由於你一旦調用它,就必須等待,直到它給你結果,不能去幹其餘事情。

若是你一邊每隔10秒讓一位同窗起跑,直到全部同窗出發完畢;另外一邊每有一個同窗回到終點就記錄成
績,直到全部同窗都跑完。恭喜你,你已經掌握了異步非阻塞模式。

你設計了兩個函數,其中一個函數記錄起跑時間和學生號,該函數你會主動調用100次;另外一個函數記
錄到達時間和學生號,該函數是一個事件驅動的callback函數,當有同窗到達終點時,你會被動調用。
你主動調用的函數是異步的,由於你調用它,它並不會告訴你結果;這個函數也是非阻塞的,由於你一
旦調用它,它就立刻返回,你不用等待就能夠再次調用它。但僅僅將這個函數調用100次,你並無完
成你的測驗任務,你還須要被動等待調用另外一個函數100次。

固然,你立刻就會意識到,同步阻塞模式的效率明顯低於異步非阻塞模式。那麼,誰還會使用同步阻塞
模式呢?

不錯,異步模式效率高,但更麻煩,你一邊要記錄起跑同窗的數據,一邊要記錄到達同窗的數據,並且
同窗們回到終點的次序與起跑的次序並不相同,因此你還要不停地在你的成績冊上查找學生號。忙亂之
中你每每會張冠李戴。

你可能會想出更聰明的辦法:你帶了不少塊秒錶,讓同窗們分組互相測驗。恭喜你!你已經掌握了多線
程同步模式!

每一個拿秒錶的同窗均可以獨立調用你的同步函數,這樣既不容易出錯,效率也大大提升,只要秒錶足夠
多,同步的效率也能達到甚至超過異步。

能夠理解,你現的問題多是:既然多線程同步既快又好,異步模式還有存在的必要嗎?

很遺憾,異步模式依然很是重要,由於在不少狀況下,你拿不出不少秒錶。你須要通訊的對端系統可能
只容許你創建一個SOCKET鏈接,不少金融、電信行業的大型業務系統都如此要求。

如今,你應該已經明白了:CAsyncSocket用於在少許鏈接時,處理大批量無步驟依賴性的業務。CSocket
用於處理步驟依賴性業務,或在可多鏈接時配合多線程使用。


2、CAsyncSocket異步機制

當你得到了一個異步鏈接後,實際上你掃除了發送動做與接收動做之間的依賴性。因此你隨時能夠發包,
也隨時可能收到包。發送、接收函數都是異步非阻塞的,頃刻就能返回,因此收發交錯進行着,你能夠
一直工做,保持很高的效率。可是,正由於發送、接收函數都是異步非阻塞的,因此僅調用它們並不能
保障發送或接收的完成。例如發送函數Send,調用它可能有4種結果:

1、錯誤,Send()==SOCKET_ERROR,GetLastError()!=WSAEWOULDBLOCK,這種狀況可能由各類網絡問題導
致,你須要立刻決定是放棄本次操做,仍是啓用某種對策

2、忙,Send()==SOCKET_ERROR,GetLastError()==WSAEWOULDBLOCK,致使這種狀況的緣由是,你的發送
緩衝區已被填滿或對方的接受緩衝區已被填滿。這種狀況你實際上不用立刻理睬。由於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);


3、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的運做流程更直觀也更簡單
相關文章
相關標籤/搜索