CAsyncSocket和CSocket

        推荐读物《Windows网络编程》 编者<美>琼斯 译者杨合庆 清华大学出版社,其中最重要的一个章节是《Winsock I/O方法》。如果本书为第一版,则是第八章;如果为第二版,则是第五章。本篇摘抄自网上的资料,略有删减。
  
        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 )   //参数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 ) //由于异步操作需要时间,不能立即完成,所以Socket返回这个错误
               {
                     //进入消息循环,以从线程消息队列里查看FD_CONNECT消息,直到收到FD_CONNECT消息,认为连接成功。
                     while( PumpMessages( FD_CONNECT ) );
                }
       }
  }
  BOOL CSocket::PumpMessages( UINT uEvent )
  {
          CWinThread* pThread = AfxGetThread();
          while( bBlocking ) //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(); //处理消息队列里的其它消息
                         pThread->OnIdle(-1);
                }
         }
  }
  BOOL CSocket::OnMessagePending()
  {
           MSG msg;
           if( PeekMessage( &msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE ) )
           { //这里仅关心WM_PAINT消息,以处理阻塞期间的主窗口重画
               ::DispatchMessage( &msg );
               return FALSE;
           }
           return FALSE;
  }

        其它的CSocket函数,诸如Send(),Receive(),Accept()都在收到WSAEWOULDBLOCK错误时,进入PumpMessages()消息循环,这样一个原本异步的CAsyncSocket,到了派生类CSocket,就变成同步的了。
        明白之后,我们可以对CSocket应用自如了。比如有些程序员将CSocket的操作放入一个线程,以实现多线程的异步Socket(通常,同步+多线程 相似于 异步 )。

posted on 2005-07-07 12:12  中士  阅读(5270)  评论(0编辑  收藏  举报

导航