要进行网络编程就要和Socket打交道,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 ); 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 );              //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 ); file://收到Socket事件消息,回调CAsyncSocket的DoCallBack()函数
    return 0L;
   }

//jqb Add

那么OnSocketNotify(WPARAM wParam, LPARAM lParam)的响应又是由谁发起的呢?

//

然而,最不容易被初学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连接上序列化数,我们要注意。

最新文章

  1. 走进AngularJs(二) ng模板中常用指令的使用方式
  2. cocos2dx 3.x(捕鱼达人炮台角度换算)
  3. 五 浅谈CPU 并行编程和 GPU 并行编程的区别
  4. 或许你不知道(2):LinkedList
  5. C++学习笔记之作用域为类的常量和作用域内的枚举
  6. 【转】G40-70、G50-70联想小新笔记本SR1000随机Linux改Windows 7系统操作指导
  7. [Mime] MediaTypes--电子邮件类型类 (转载)
  8. javascript创建对象(一)
  9. Best Time to Buy and Sell Stock III 解题思路
  10. phpMyAdmin下载与安装
  11. Configuration ReportNG with TestNG
  12. (转)MapReduce 中的两表 join 几种方案简介
  13. JUnit——(二)注解
  14. FZU 1920 Left Mouse Button 简单搜索
  15. django使用xlwt导出excel文件
  16. java——封装和关键字
  17. linux释放内存的命令
  18. 2018-05-11-机器学习环境安装-I7-GTX960M-UBUNTU1804-CUDA90-CUDNN712-TF180-KERAS-GYM-ATARI-BOX2D
  19. 自定义Banner
  20. 使用BenchmarkSQL测试PostgreSQL

热门文章

  1. 使用 U 盘装个 winXP 原版镜像玩红警
  2. 2019秋季学期第2周Java学习总结
  3. 高级UI晋升之自定义View实战(六)
  4. OpenSuSe开启sshd服务
  5. UVA 12821 Double Shortest Paths
  6. vue - blog开发学习2
  7. django 内置server 外网不能访问, 报连接超时
  8. Ti 949 配置 948 i2c
  9. Codeforces 19E&BZOJ 4424 Fairy(好题)
  10. Codeforces Round #552:G. Minimum Possible LCM