SocketAPI,CAsyncSocket,CSocket內幕及其用法

來源:互聯網
上載者:User
SocketAPI,CAsyncSocket,CSocket內幕及其用法2010-04-20 14:46

    Socket有同步阻塞方式和非同步非阻塞方式兩種使用,事實上同步和非同步在我們編程的生涯中可能遇到了很多,而Socket也沒什麼特別。雖然同步好用,不費勁,但不能滿足一些應用場合,其效率也很低。
    也許初涉編程的人不能理解“同步(或阻塞)”和“非同步(或非阻塞)”,其實簡單兩句話就能講清楚,同步和非同步往往都是針對一個函數來說的,“同步”就是函數直到其要執行的功能全部完成時才返回,而“非同步”則是,函數僅僅做一些簡單的工作,然後馬上返回,而它所要實現的功能留給別的線程或者函數去完成。例如,SendMessage就是“同步”函數,它不但發送訊息到訊息佇列,還需要等待訊息被執行完才返回;相反PostMessage就是個非同步函數,它只管發送一個訊息,而不管這個訊息是否被處理,就馬上返回。

一、Socket API
    首先應該知道,有Socket1.1提供的原始API函數,和Socket2.0提供的一組擴充函數,兩套函數。這兩套函數有重複,但是2.0提供的函數功能更強大,函數數量也更多。這兩套函數可以靈活混用,分別包含在標頭檔Winsock.h,Winsock2.h,分別需要引入庫 wsock32.lib、Ws2_32.lib。

1、預設用作同步阻塞方式,那就是當你從不調用WSAIoctl()和ioctlsocket()來改變Socket IO模式,也從不調用WSAAsyncSelect()和WSAEventSelect()來選擇需要處理的Socket事件。正是由於函數accept (),WSAAccept(),connect(),WSAConnect(),send(),WSASend(),recv(),WSARecv()等函數被用作阻塞方式,所以可能你需要放在專門的線程裡,這樣以不影響主程式的運行和主視窗的重新整理。
2、如果作為非同步用,那麼程式主要就是要處理事件。它有兩種處理事件的辦法:
    第一種,它常關聯一個視窗,也就是非同步Socket的事件將作為訊息發往該視窗,這是由WinSock擴充規範裡的一個函數WSAAsyncSelect()來實現和視窗關聯。最終你只需要處理視窗訊息,來收發資料。
   第二種,用到了擴充規範裡另一個關於事件的函數WSAEventSelect(),它是用事件對象的方式來處理Socket事件,也就是,你必須首先用 WSACreateEvent()來建立一個事件對象,然後調用WSAEventSelect()來使得Socket的事件和這個事件對象關聯。最終你將要在一個線程裡用WSAWaitForMultipleEvents()來等待這個事件對象被觸發。這個過程也稍顯複雜。
二、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 ); //建立Socket本身

   CSocketWnd* pSockWnd = new CSocketWnd; //建立響應事件的視窗,實際的這個視窗在AfxSockInit()調用時就被建立了。
   pSockWnd->Create(...);

   WSAAsyncSelect( m_hSocket, pSockWnd->m_hWnd, WM_SOCKET_NOTIFY, lEvent ); //Socket事件和視窗關聯
}

static void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
{
   CAsyncSocket Socket;
   Socket.Attach( (SOCKET)wParam ); wParam就是觸發這個事件的Socket的控制代碼
   int nErrorCode = WSAGETSELECTERROR(lParam); 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 ); //收到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。

三、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(通常,同步+多線程 相似於 非同步 )。

四、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就是個比較好的非同步方式封裝。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.