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就是个比较好的异步方式封装。
相关文章
相关标签/搜索