SocketAPI,CAsyncSocket,CSocket內幕及其用法

    Socket有同步阻塞方式和異步非阻塞方式兩種使用,事實上同步和異步在咱們編程的生涯中可能遇到了不少,而Socket也沒什麼特別。雖然同步好用,不費勁,但不能知足一些應用場合,其效率也很低。
    也許初涉編程的人不能理解「同步(或阻塞)」和「異步(或非阻塞)」,其實簡單兩句話就能講清楚,同步和異步每每都是針對一個函數來講的,「同步」就是函 數直到其要執行的功能所有完成時才返回,而「異步」則是,函數僅僅作一些簡單的工做,而後立刻返回,而它所要實現的功能留給別的線程或者函數去完成。例 如,SendMessage就是「同步」函數,它不但發送消息到消息隊列,還須要等待消息被執行完才返回;相反PostMessage就是個異步函數,它 只管發送一個消息,而無論這個消息是否被處理,就立刻返回。
1、Socket API
    首先應該知道,有Socket1.1提供的原始API函數,和Socket2.0提供的一組擴展函數,兩套函數。這兩套函數有重複,可是2.0提供的函數 功能更強大,函數數量也更多。這兩套函數能夠靈活混用,分別包含在頭文件Winsock.h,Winsock2.h,分別須要引入庫 wsock32.lib、Ws2_32.lib。
    一、默認用做同步阻塞方式,那就是當你從不調用WSAIoctl()和ioctlsocket()來改變Socket IO模式,也從不調用WSAAsyncSelect()和WSAEventSelect()來選擇須要處理的Socket事件。正是因爲函數accept (),WSAAccept(),connect(),WSAConnect(),send(),WSASend(),recv(),WSARecv()等 函數被用做阻塞方式,因此可能你須要放在專門的線程裏,這樣以不影響主程序的運行和主窗口的刷新。
    二、若是做爲異步用,那麼程序主要就是要處理事件。它有兩種處理事件的辦法:
    第一種,它常關聯一個窗口,也就是異步Socket的事件將做爲消息發往該窗口,這是由WinSock擴展規範裏的一個函數WSAAsyncSelect()來實現和窗口關聯。最終你只須要處理窗口消息,來收發數據。
   第二種,用到了擴展規範裏另外一個關於事件的函數WSAEventSelect(),它是用事件對象的方式來處理Socket事件,也就是,你必須首先用 WSACreateEvent()來建立一個事件對象,而後調用WSAEventSelect()來使得Socket的事件和這個事件對象關聯。最終你將 要在一個線程裏用WSAWaitForMultipleEvents()來等待這個事件對象被觸發。這個過程也稍顯複雜。
2、CAsyncSocket
    看類名就知道,它是一個異步非阻塞Socket封裝類,CAsyncSocket::Create()有一個參數指明瞭你想要處理哪些Socket事件, 你關心的事件被指定之後,這個Socket默認就被用做了異步方式。那麼CAsyncSocket內部究竟是如何將事件交給你的呢?
    CAsyncSocket的Create()函數,除了建立了一個SOCKET之外,還建立了個CSocketWnd窗口對象,並使用 WSAAsyncSelect()將這個SOCKET與該窗口對象關聯,以讓該窗口對象處理來自Socket的事件(消息),然而CSocketWnd收 到Socket事件以後,只是簡單地回調CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(), CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虛函數。因此CAsyncSocket的派生 類,只須要在這些虛函數裏添加發送和接收的代碼。
  簡化後,大體的代碼爲:
 
bool CAsyncSocket::Create( long lEvent ) file: //參數lEvent是指定你所關心的Socket事件
    {
     m_hSocket = socket( PF_INET, SOCK_STREAM, 0 ); file: //建立Socket自己
     CSocketWnd* pSockWnd = new CSocketWnd; file: //建立響應事件的窗口,實際的這個窗口在AfxSockInit()調用時就被建立了。
     pSockWnd->Create(...);
     WSAAsyncSelect( m_hSocket, pSockWnd->m_hWnd, WM_SOCKET_NOTIFY, lEvent ); file: //Socket事件和窗口關聯
    }

     static void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
    {
     CAsyncSocket Socket;
     Socket.Attach( (SOCKET)wParam ); file: //wParam就是觸發這個事件的Socket的句柄
     int nErrorCode = WSAGETSELECTERROR(lParam); file: //lParam是錯誤碼與事件碼的合成
     switch (WSAGETSELECTEVENT(lParam))
     {
     case FD_READ:
        pSocket->OnReceive(nErrorCode);
         break;
     case FD_WRITE:
        pSocket->OnSend(nErrorCode);
         break;
     case FD_OOB:
        pSocket->OnOutOfBandData(nErrorCode);
         break;
     case FD_ACCEPT:
        pSocket->OnAccept(nErrorCode);
         break;
     case FD_CONNECT:
        pSocket->OnConnect(nErrorCode);
         break;
     case FD_CLOSE:
        pSocket->OnClose(nErrorCode);
         break;
     }
    }
    CSocketWnd類大體爲:
    BEGIN_MESSAGE_MAP(CSocketWnd, CWnd)
     ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify)
    END_MESSAGE_MAP()
    LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
    {
     CAsyncSocket::DoCallBack( wParam, lParam ); file: //收到Socket事件消息,回調CAsyncSocket的DoCallBack()函數
     return 0L;
    }

    然而,最不容易被初學Socket編程的人理解的,也是本文最要提醒的一點是,客戶方在使用CAsyncSocket::Connect() 時,每每返回一個WSAEWOULDBLOCK的錯誤(其它的某些函數調用也如此),實際上這不該該算做一個錯誤,它是Socket提醒咱們,因爲你使用 了非阻塞Socket方式,因此(鏈接)操做須要時間,不能瞬間創建。既然如此,咱們能夠等待呀,等它鏈接成功爲止,因而許多程序員就在調用 Connect()以後,Sleep(0),而後不停地用WSAGetLastError()或者CAsyncSocket::GetLastError ()查看Socket返回的錯誤,直到返回成功爲止。這是一種錯誤的作法,斷言,你不能達到預期目的。事實上,咱們能夠在Connect()調用以後等待 CAsyncSocket::OnConnect()事件被觸發,CAsyncSocket::OnConnect()是要代表Socket要麼鏈接成功 了,要麼鏈接完全失敗了。至此,咱們在CAsyncSocket::OnConnect()被調用以後就知道是否Socket鏈接成功了,仍是失敗了。
    相似的,Send()若是返回WSAEWOULDBLOCK錯誤,咱們在OnSend()處等待,Receive()若是返回WSAEWOULDBLOCK錯誤,咱們在OnReceive()處等待,以此類推。
    還有一點,也許是個難點,那就是在客戶方調用Connect()鏈接服務方,那麼服務方如何Accept(),以創建鏈接的問題。簡單的作法就是在監聽的Socket收到OnAccept()時,用一個新的CAsyncSocket對象去創建鏈接,例如:
 void CMySocket::OnAccept( int ErrCode )
 {
       CMySocket* pSocket = new CMySocket;
       Accept( *pSocket );
 }
    因而,上面的pSocket和客戶方創建了鏈接,之後的通訊就是這個pSocket對象去和客戶方進行,而監聽的Socket仍然繼續在監聽,一旦又有一 個客戶方要鏈接服務方,則上面的OnAccept()又會被調用一次。固然pSocket是和客戶方通訊的服務方,它不會觸發OnAccept()事件, 由於它不是監聽Socket。
3、CSocket
   CSocket是MFC在CAsyncSocket基礎上派生的一個同步阻塞Socket的封裝類。它是如何又把CAsyncSocket變成同步的,並且還能響應一樣的Socket事件呢?
   其實很簡單,CSocket在Connect()返回WSAEWOULDBLOCK錯誤時,不是在OnConnect(),OnReceive()這些事 件終端函數裏去等待。你先必須明白Socket事件是如何到達這些事件函數裏的。這些事件處理函數是靠CSocketWnd窗口對象回調的,而窗口對象收 到來自Socket的事件,又是靠線程消息隊列分發過來的。總之,Socket事件首先是做爲一個消息發給CSocketWnd窗口對象,這個消息確定需 要通過線程消息隊列的分發,最終CSocketWnd窗口對象收到這些消息就調用相應的回調函數(OnConnect()等)。
   因此,CSocket在調用Connect()以後,若是返回一個WSAEWOULDBLOCK錯誤時,它立刻進入一個消息循環,就是從當前線程的消息隊 列裏取關心的消息,若是取到了WM_PAINT消息,則刷新窗口,若是取到的是Socket發來的消息,則根據Socket是否有操做錯誤碼,調用相應的 回調函數(OnConnect()等)。
  
大體的簡化代碼爲:
BOOL CSocket::Connect( ... )
    {
     if( !CAsyncSocket::Connect( ... ) )
     {
         if( WSAGetLastError() == WSAEWOULDBLOCK ) file: //因爲異步操做須要時間,不能當即完成,因此Socket返回這個錯誤
        {
         file: //進入消息循環,以從線程消息隊列裏查看FD_CONNECT消息,直到收到FD_CONNECT消息,認爲鏈接成功。
         while( PumpMessages( FD_CONNECT ) );
        }
     }
    }
    BOOL CSocket::PumpMessages( UINT uEvent )
    {
            CWinThread* pThread = AfxGetThread();
             while( bBlocking ) file: //bBlocking僅僅是一個標誌,看用戶是否取消對Connect()的調用
            {
                    MSG msg;
                     if( PeekMessage( &msg, WM_SOCKET_NOTIFY ) )
                    {
                         if( msg.message == WM_SOCKET_NOTIFY && WSAGETSELECTEVENT(msg.lParam) == uStopFlag )
                         {
                                 CAsyncSocket::DoCallBack( msg.wParam, msg.lParam );
                                 return TRUE;
                         }        
                 }
                 else
                {
                         OnMessagePending(); file: //處理消息隊列裏的其它消息
                         pThread->OnIdle(-1);
                }
         }
    }
    BOOL CSocket::OnMessagePending()
    {
            MSG msg;
             if( PeekMessage( &msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE ) )
             { file: //這裏僅關心WM_PAINT消息,以處理阻塞期間的主窗口重畫
                     ::DispatchMessage( &msg );
                     return FALSE;
             }
             return FALSE;
    }
 
   其它的CSocket函數,諸如Send(),Receive(),Accept()都在收到WSAEWOULDBLOCK錯誤時,進入 PumpMessages()消息循環,這樣一個本來異步的CAsyncSocket,到了派生類CSocket,就變成同步的了。
  明白以後,咱們能夠對CSocket應用自如了。好比有些程序員將CSocket的操做放入一個線程,以實現多線程的異步Socket(一般,同步+多線程 類似於 異步 )。
4、CSocketFile
  另外,進行Socket編程,不能不提到CSocketFile類,其實它並非用來在Socket雙方發送文件的,而是將須要序列化的數據,好比一些結 構體數據,傳給對方,這樣,程序的CDocument()的序列化函數就徹底能夠和CSocketFile 聯繫起來。例如你有一個CMyDocument實現了Serialize(),你能夠這樣來將你的文檔數據傳給Socket的另外一方:
 CSocketFile file( pSocket );
 CArchive ar( &file, CArchive::store );
 pDocument->Serialize( ar );
 ar.Close();
  一樣,接收一方能夠只改變上面的代碼爲CArchive ar( &file, CArchive::load );便可。
   注意到,CSocketFile類雖然從CFile派生,但它屏蔽掉了CFile::Open()等函數,而函數裏僅扔出一個例外。那麼也就是說,你不能 調用CSocketFile的Open函數來打開一個實實在在的文件,不然會致使例外,若是你須要利用CSocketFile來傳送文件,你必須提供 CSocketFile類的這些函數的實現。
    再一點,CArchive不支持在datagram的Socket鏈接上序列化數據。

    找了好些文章,感受這篇挺容易懂的。本來WinSock API是同步模式,若是網絡出了問題或者其它緣由他會一直阻塞,緣由就是太過簡單的同步模式,它是讓像send()這樣的調用一直處於線程掛起狀態,英特 網上的通訊可不像在PC裏CPU跟外設的通訊同樣簡單,好比咱們打開一個網頁,不可能一下就打開,阻塞是常有的事,不少時候咱們都在等,等待你的PC連到 外面集線器的那根線上何時有信號過來,事實是這樣,須要等待,可是咱們不能讓線程傻傻地等待,採用多線程的方法,讓另一個線程等待,設定超時計時器 等等方法,讓原來的線程能夠幹別的事,好比寫一行字‘正在鏈接...’,這就是異步方式,CAsyncSocket就是個比較好的異步方式封裝。
相關文章
相關標籤/搜索