微软的MFC把复杂的WinSock API函数封装到类里,这使得编写网络应用程序更容易。

  CAsyncSocket类逐个封装了WinSock API,为高级网络程序员提供了更加有力而灵活的方法。这个类基于程序员了解网络通讯的假设,目的是为了在MFC中使用WinSock,程序员有责任处理诸如阻塞、字节顺序和在Unicode与MBCS 间转换字符的任务。为了给程序员提供更方便的接口以自动处理这些任务,MFC给出了CSocket类,这个类是由CAsyncSocket类继承下来的,它提供了比CAsyncSocket更高层的WinSock API接口。CSocket类和CSocketFile类可以与CArchive类一起合作来管理发送和接收的数据,这使管理数据收发更加便利。CSocket对象提供阻塞模式,这对于Carchive的同步操作是至关重要的。阻塞函数(如Receive()、Send()、ReceiveFrom()、SendTo() 和Accept())直到操作完成后才返回控制权,因此如果需要低层控制和高效率,就使用CAsyncSocket类;如果需要方便,则可使用CSocket类。

  CSocket类是由CAsyncSocket继承而来的,事实上,在MFC中CAsyncSocket 逐个封装了WinSock API,每个CAsyncSocket对象代表一个Windows Socket对象,使用CAsyncSocket 类要求程序员对网络编程较为熟悉。相比起来,CSocket类是CAsyncSocket的派生类,继承了它封装的WinSock API。

  一个CSocket对象代表了一个比CAsyncSocket对象更高层次的Windows Socket的抽象,CSocket类与CSocketFile类和CArchive类一起工作来发送和接收数据,因此使用它更加容易使用。CSocket对象提供阻塞模式,因为阻塞功能对于CArchive的同步操作是至关重要的。在这里有必要对阻塞的概念作一解释:一个socket可以处于"阻塞模式"或"非阻塞模式",当一个套接字处于阻塞模式(即同步操作)时,它的阻塞函数直到操作完成才会返回控制权,之所以称为阻塞是因为此套接字的阻塞函数在完成操作返回之前什么也不能做。如果一个socket处于非阻塞模式(即异步操作),则会被调用函数立即返回。在CAsyncSocket类中可以用GetLastError 成员函数查询最后的错误,如果错误是WSAEWOULDBLOCK则说明有阻塞,而CSocket绝不会返回WSAEWOULDBLOCK,因为它自己管理阻塞。微软建议尽量使用非阻塞模式,通过网络事件的发生而通知应用程序进行相应的处理。但在CSocket类中,为了利用CArchive 处理通讯中的许多问题和简化编程,它的一些成员函数总是具有阻塞性质的,这是因为CArchive类需要同步的操作。

  在Win32环境下,如果要使用具有阻塞性质的套接字,应该放在独立的工作线程中处理,利用多线程的方法使阻塞不至于干扰其他线程,也不会把CPU时间浪费在阻塞上。多线程的方法既可以使程序员享受CSocket带来的简化编程的便利,也不会影响用户界面对用户的反应。

  CAsyncSocket类编程模型
  在一个MFC应用程序中,要想轻松处理多个网络协议,而又不牺牲灵活性时,可以考虑使用CAsyncSocket类,它的效率比CSocket 类要高。CAsyncSocket类针对字节流型套接字的编程模型简述如下:
  1、构造一个CAsyncSocket对象,并用这个对象的Create成员函数产生一个Socket句柄。可以按如下两种方法构造: 
  CAsyncSocket sock; //使用默认参数产生一个字节流套接字
  Sock.Create(); 
  或在指定端口号产生一个数据报套接字
  CAsyncSocket* pSocket = new CAsyncSocket;
  int nPort = 27;
  pSocket->Create(nPort, SOCK_DGRAM);
  第一种方法在栈上产生一个CAsyncSocket对象,而第二种方法在堆上产生CAsyncSocket对象;

  第一种方法中Create()成员函数用缺省参数产生一个字节流套接字,第二种方法中用Create()成员函数在指定的端口产生一个数字报套接字。

  Create()函数的原型为:
  BOOL Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM,
  LPCTSTR lpszSocketAddress = NULL );
  该函数的参数有:
  1)端口,UINT类型。注意:如果是服务方,则使用一个众所周知的端口供服务方连接;如果是客户方,典型做法是接受默认参数,使套接字可以自主选择一个可用端口;
  2)socket 类型,可以是SOCK-STREAM(默认值,字节流)或SOCK-DGRAM(数据报); 
  3)socket的地址,例如"ftp.gliet.edu.cn"或"202.193.64.33"。 
  2、如是客户方程序,用CAsyncSocket∷Connect()成员函数连接到服务方;如是服务方程序,用CAsyncSocket∷Listen()成员函数开始监听,一旦收到连接请求,则调用CAsyncSocket∷Accept()成员函数开始接收。注意:CAsyncSocket ∷Accept()成员函数要用一个新的并且是空的CAsyncSocket对象作为它的参数,这里所说的"空的"指的是这个新对象还没有调用Create()成员函数。 
  3、调用其他的CAsyncSocket类的Receive()、ReceiveFrom()、Send()和SendTo()等成员函数进行数据通信。 
  4、通讯结束后,销毁CAsyncSocket对象。如果是在栈上产生的CAsyncSocket对象,则对象超出定义的范围时自动被析构;如果是在堆上产生,也就是用了new这个操作符,则必须使用delete操作符销毁CAsyncSocket 对象。 
  

  CSocket类编程模型
  使用CSocket对象涉及CArchive和CSocketFile 类对象。以下介绍的针对字节流型套接字的操作步骤中,只有第3步对于客户方和服务方操作是不同的,其他步骤都相同。 
  1、构造一个CSocket对象。 
  2、使用这个对象的Create()成员函数产生一个socket对象。在客户方程序中,除非需要数据报套接字,Create()函数一般情况下应该使用默认参数。而对于服务方程序,必须在调用Create时指定一个端口。需要注意的是,Carchive类对象不能与数据报(UDP)套接字一起工作,因此对于数据报套接字,CAsyncSocket和CSocket 的使用方法是一样的。 
  3、如果是客户方套接字,则调用CAsyncSocket ∷Connect()函数与服务方套接字连接;如果是服务方套接字,则调用CAsyncSocket∷Listen()开始监听来自客户方的连接请求,收到连接请求后,调用CAsyncSocket∷Accept()函数接受请求,建立连接。请注意Accept()成员函数需要一个新的并且为空的CSocket对象作为它的参数,解释同上。 
  4、产生一个CSocketFile对象,并把它与CSocket 对象关联起来。
  5、为接收和发送数据各产生一个CArchive 对象,把它们与CSocketFile对象关联起来。切记CArchive是不能和数据报套接字一起工作的。 
  6、使用CArchive对象的Read()、Write()等函数在客户与服务方传送数据。
  7、通讯完毕后,销毁CArchive、CSocketFile和CSocket对象

[转]CAsyncSocket和CSocket

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(通常,同步+多线程 相似于 异步 )。

【转】本文主要针对MFC库中的CAsyncSocket类和CSocket类,才疏学浅,权当做抛砖引玉。

  这里所列出的问题主要是在最近编写基于IP网的语音聊天程序的过程中碰到的,不一定很具代表性,仅供参考和讨论。对于从事Win32平台的VOIP开发工作的朋友们,或许会有点利用价值。

  1. CSocket类和CAsyncSocket类对多线程的支持问题
  Winsocks本身是支持多线程的,具有一定的线程独立性和安全性,但CSocket类以及CAsyncSocket类都有一些线程安全性问题。比如在线程之间传递CSocket对象就会发生异常,MSDN中的建议是,传递前Detach,传递socket句柄,在目标线程中再Attach到一个CSocket对象上。这似乎说明CSocket(或CAsyncSocket)具有线程依赖性。然而情况并不是绝对的,我的程序中出现的两种传递CAsyncSocket或CSocket对象的情况没有发生异常,也没有运行效率的问题:
  (1) 在主线程中创建和关闭一个CSocket阻塞型数据报套接字,在另一线程中用它发送数据(SendTo)。若用它接收数据(ReceiveFrom)则出现异常。
  2) 在主线程中创建和关闭一个CAsyncSocket非阻塞型数据报套接字,在另一线程中用它接收数据(ReceiveFrom)也没有问题了。
  上述两种情况,我的感觉是毫无道理可言,但在我的机器上运行却是非常稳定。我的建议是,如果使用CAsyncSocket类和CSocket类,尽量不要跨线程使用(我以后也不会铤而走险了)。

  2. 阻塞和非阻塞的选择问题
  阻塞套接字使用方便,但会影响到线程的消息响应,比较适合于用流式套接字传送大量数据的情况。
  对于实时语音数据的传送来说,数据的发送受制于音频采集设备,一旦有数据可发送,才调用套接字来发送,因而套接字的阻塞与否对线程的运行时间占用不会太大(我的程序中一个数据包的大小只有20KB,发送的过程很快)。而在接收方,音频回放设备受制于接收套接字。在没有接受到任何数据时,整个线程却卡在ReceiveForm()上,无法实现正常的消息循环,如此时向该线程发出WM_QUIT消息,它将无法得到执行。同样,在发送方,为了不阻塞发送线程的消息循环,音频采集设备也应该是非阻塞(异步)的。

05-11 23:04