一雨田的专栏

伟人将复杂的事情变简单,小人将简单的事情变复杂

用户操作
[即时聊天] [发私信] [加为好友]
一雨田ID:dylgsy
91678次访问,排名1090,好友2人,关注者13人。
一雨田
dylgsy的文章
原创 41 篇
翻译 1 篇
转载 6 篇
评论 292 篇
最近评论
heray818:我的邮箱是:heray818@126.com
heray818:你好 能否也传我一份 谢谢了啊
救援隊募集:アダルトエロ不倫
モテ度審査員:童貞セフレナンパ
temp:很好
文章分类
收藏
    相册
    好友Blog
    圈内朋友
    sankt的专栏
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创  Socket在阻塞模式下的信息收发和文件接收收藏

    新一篇: 目录的打包和解包 | 旧一篇: Windows界面——使用Custom Draw优雅的实现ListCtrl的重绘

     Socket在阻塞模式下的信息收发和文件接收

    概述:

    收发数据是网络编程的主题,在套接字上收发数据我们可以使用send和recv,当然还有Winsock2的WSASend和WSARecv。我们这里只讨论send和recv。

    套接字可以工作在阻塞态和非阻塞态,,阻塞态就是函数调用会停住,非阻塞态就是函数调用会立刻返回,待到后面的某个时间点在去取得结果。我们这里先讨论阻塞态。

    收发信息就是在Socket上收发二进制流。而收发文件实际上也就是收发信息,只是多了打开文件,把文件数据读成二进制流传到SOCKET上去和在接收端把二进制流写进一个文件里去这两步。

    为什么要写这篇文章:

    为什么要讨论这个主题呢?因为我在一个项目中使用信息收发的时候,发现会出现一些莫名其妙的现象,就是有时候某台机器可以顺利运行,但另外的机器又不能好好执行,而某台机器有时候正常运行,有时候在运行时又会出现运行时的错误。

    我知道这肯定是一些内存的操作引起的,这涉及到具体的项目内容就不谈了,这里重点说一下如何正确的进行SOCKET上信息的收发和文件的收发。如果没耐心的人可以直接到下面去看代码,把这些函数运用到MFC程序里去会省去很多事。(代码用了Cstring,所以只适合MFC程序,如果要其他用途可以根据函数的算法来重写)。

    关键问题(Key Point):

    关键问题在于send和recv这两个函数。

    send函数原型:

    int send(
      SOCKET s,             
      const char FAR *buf, 
      int len,              
      int flags             
    );

    这里S是套接字句柄,buf是存放准备发出去数据的缓冲区,长度是缓冲区中数据的长度。Flag给0吧。暂时不管(可以查看MSDN)。返回值是实际发出去的字节长度。(在非阻塞态实际发出的字节数可能会比指定的len的数量少,这是另一种情况)。

    这就很好理解了,我给出一个本地的缓冲区,在len那里指定数据的长度,然后通过send 就发出去。这有什么难呢?别急,我们再来看看接收函数recv。

    recv函数原型:

    int recv(
      SOCKET s,      
      char FAR *buf, 
      int len,       
      int flags      
    );

    这里S是套接字句柄,buf是存放准备接收数据的缓冲区,长度是缓冲区的长度。Flag给0吧。暂时不管(可以查看MSDN)。返回值是实际接收到的字节长度。

    问题就是这个recv收到的字节数不一定和我们给出的buf的字节数一样。会根据网络状态随机变化。那我们如何在发送端和接收端协调好这种字节的收发呢?(要知道,阻塞态的套接字讲究的就是字节的协调,就是这个阶段我给你多少字节,你就只能接收多少字节,多一个或者少一个都会把后面的字节流打乱了)。

    所以我们要想个办法来定义好字节数量的收发。定个协议,检测头尾?也可以,这可以自己去发挥。为了说明问题,我们这里用的是最土的方法,就是每次都发送300个字节,接收方也接收300字节。

    在接收方能不能这样?

    recv(hSocket, szBuf,  300, 0)

    这样就能把300个字节接收过来了吗?事实证明这样是不行的,你可能收到200、220、150等等没规律的数字,如果这样,那SOCKET上的字节流岂不是被搞乱了,对,就是被搞乱了,随之而来的就是程序的崩溃,莫名其妙的错误。

    收发信息--解决方法

    我们要如何解决这个问题呢?

    首先第一个想法就是,如果收的字节没达到我的要求数目时,要继续接收,并且填在缓冲区中。好这个想法完全正确,那如何在代码中表现出来呢?首先我需要一个变量表示接收了多少,或者还有多少没接收,然后循环的接收,直到满足了我要接收的字节数,这才算一次成功的接收。收发的代码如下:

    /*****************************************************************************\
     * hSocket:   套接字句柄
     * nRecvSize: 需要接收的字节数
     * strOut:    用字串返回接收到的字节流
    \*****************************************************************************/
    BOOL CDGSocket::Recv(SOCKET hSocket, int nRecvSize, CString &strOut)
    {
     BOOL bRet = TRUE;
     char *pszBuf = new char[nRecvSize];
     int nLeftToRecv = nRecvSize;

     do
     {
      // 取得要开始写入的地址
      char *pLeftBuf = (char *) ( pszBuf + (nRecvSize - nLeftToRecv) );

      // 接收字节流
      int nRecvBytes = recv(hSocket, pLeftBuf, nLeftToRecv, 0);
      
      // 判断一下是否出错了
      if(nRecvBytes == SOCKET_ERROR)
      {
       MyOutput("My Recv Error!");
       bRet = FALSE;
       goto FINAL;   
      }
      
      nLeftToRecv -= nRecvBytes;
      
     } while(nLeftToRecv > 0);

        strOut = pszBuf;

    FINAL:
     delete []pszBuf;
     return bRet;
    }

    /*****************************************************************************\
     * hSocket:   套接字句柄
     * nRecvSize: 需要发送的字节数
     * pStr:      要发送出去的字节流
    \*****************************************************************************/
    BOOL CDGSocket::Send(SOCKET hSocket, int nSendSize, LPCTSTR pStr)
    {
     BOOL bRet = TRUE;

     if(send(hSocket, pStr, nSendSize, 0) == SOCKET_ERROR)
     {
             MyOutput("My Send Error!");
      bRet = FALSE;
     }

     return bRet;
    }

     

    收发文件--解决方法

    上面已经说过收发文件实际也是收发信息,只是多了把文件信息变为字节流这一步。下面给出了配对的示例代码:

    /*****************************************************************************\
     * hSocket:   套接字句柄
     * fName:    发送的本地文件路径
    \*****************************************************************************/
    BOOL CDGSocket::SendFile(SOCKET hSocket, CString fName)
    {
     BOOL bRet = TRUE;
     int fileLength, cbLeftToSend;

     // pointer to buffer for sending data (memory is allocated after sending file size)
     BYTE* sendData = NULL;
     CFile sourceFile;
     CFileException fe;
     BOOL bFileIsOpen = FALSE;

     if( !( bFileIsOpen = sourceFile.Open( fName, CFile::modeRead | CFile::typeBinary, &fe ) ) )
     {
      TCHAR strCause[256];
      fe.GetErrorMessage( strCause, 255 );
      TRACE( "SendFileToRemoteRecipient encountered an error while opening the local file\n"
       "\tFile name = %s\n\tCause = %s\n\tm_cause = %d\n\tm_IOsError = %d\n",
       fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError );
      
      /* you should handle the error here */
      
      bRet = FALSE;
      goto PreReturnCleanup;
     }
     
     // first send length of file
     fileLength = sourceFile.GetLength();
     fileLength = htonl( fileLength );
     cbLeftToSend = sizeof( fileLength );
     
     do
     {
      int cbBytesSent;
      const char* bp = (const char*)(&fileLength) + sizeof(fileLength) - cbLeftToSend;
      cbBytesSent = send(hSocket, bp, cbLeftToSend, 0);
      
      // test for errors and get out if they occurred
      if ( cbBytesSent == SOCKET_ERROR )
      {
       int iErr = ::GetLastError();
       TRACE( "SendFileToRemoteRecipient returned a socket error while sending file length\n"
        "\tNumber of Bytes sent = %d\n"
        "\tGetLastError = %d\n", cbBytesSent, iErr );
       
       /* you should handle the error here */

       bRet = FALSE;
       goto PreReturnCleanup;
      }
      
      // data was successfully sent, so account for it with already-sent data
      cbLeftToSend -= cbBytesSent;
     }
     while ( cbLeftToSend > 0 );
     
     // now send the file's data
     
     sendData = new BYTE[SEND_BUFFER_SIZE];
     
     cbLeftToSend = sourceFile.GetLength();
     
     do
     {
      // read next chunk of SEND_BUFFER_SIZE bytes from file
      
      int sendThisTime, doneSoFar, buffOffset;
      
      sendThisTime = sourceFile.Read( sendData, SEND_BUFFER_SIZE );
      buffOffset = 0;
      
      do
      {
       doneSoFar = send(hSocket, (const char*)(sendData + buffOffset), sendThisTime, 0);
       
       // test for errors and get out if they occurred
       if ( doneSoFar == SOCKET_ERROR )
       {
        int iErr = ::GetLastError();
        TRACE( "SendFileToRemoteRecipient returned a socket error while sending chunked file data\n"
         "\tNumber of Bytes sent = %d\n"
         "\tGetLastError = %d\n", doneSoFar, iErr );
        
        /* you should handle the error here */
        
        bRet = FALSE;
        goto PreReturnCleanup;
       }
       
    /***************************
      un-comment this code and put a breakpoint here to prove to yourself that sockets can send fewer bytes than requested
        
       if ( doneSoFar != sendThisTime )
       {
        int ii = 0;
       }
    ****************************/
       
       // data was successfully sent, so account for it with already-sent data
       
       buffOffset += doneSoFar;
       sendThisTime -= doneSoFar;
       cbLeftToSend -= doneSoFar;
      }
      while ( sendThisTime > 0 );
      
     }
     while ( cbLeftToSend > 0 );
     
     
    PreReturnCleanup:  // labelled goto destination
     
     // free allocated memory
     // if we got here from a goto that skipped allocation, delete of NULL pointer
     // is permissible under C++ standard and is harmless
     delete[] sendData;
     
     if ( bFileIsOpen )
      sourceFile.Close();  // only close file if it's open (open might have failed above)
     
     return bRet;
    }


    /*****************************************************************************\
     * hSocket:   套接字句柄
     * fName:    要接收到本地的文件路径
    \*****************************************************************************/
    BOOL CDGSocket::RecvFile(SOCKET hSocket, CString fName)
    {
     BOOL bRet = TRUE;        // return value
     
     int dataLength, cbBytesRet, cbLeftToReceive; // used to monitor the progress of a receive operation
     
     BYTE* recdData = NULL; // pointer to buffer for receiving data (memory is allocated after obtaining file size)
     
     CFile destFile;
     CFileException fe;
     BOOL bFileIsOpen = FALSE;
     
     // open/create target file that receives the transferred data
     
     if( !( bFileIsOpen = destFile.Open( fName, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary, &fe ) ) )
     {
      TCHAR strCause[256];
      fe.GetErrorMessage( strCause, 255 );
      
      MyOutput(fName);

      CString strErrMsg;
      strErrMsg.Format("GetFileFromRemoteSender encountered an error while opening the local file\n"
       "\tFile name = %s\n\tCause = %s\n\tm_cause = %d\n\tm_IOsError = %d\n",
       fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError);

      MyOutput( strErrMsg );
      
      /* you should handle the error here */
      
      bRet = FALSE;
      goto PreReturnCleanup;
     }
     
     
     // get the file's size first
     
     cbLeftToReceive = sizeof( dataLength );
     
     do
     {
      char* bp = (char*)(&dataLength) + sizeof(dataLength) - cbLeftToReceive;
      cbBytesRet = recv(hSocket, bp, cbLeftToReceive, 0);
      
      // test for errors and get out if they occurred
      if ( cbBytesRet == SOCKET_ERROR || cbBytesRet == 0 )
      {
       int iErr = ::GetLastError();
       CString strErr;
       strErr.Format("GetFileFromRemoteSite returned a socket error while getting file length\n"
        "\tNumber of Bytes received (zero means connection was closed) = %d\n"
        "\tGetLastError = %d\n", cbBytesRet, iErr );
      
       /* you should handle the error here */
       
       MyOutput(strErr);

       bRet = FALSE;
       goto PreReturnCleanup;
      }
      
      // good data was retrieved, so accumulate it with already-received data
      cbLeftToReceive -= cbBytesRet;
      
     }
     while ( cbLeftToReceive > 0 );
     
     dataLength = ntohl( dataLength );
     
     
     // now get the file in RECV_BUFFER_SIZE chunks at a time
     
     recdData = new byte[RECV_BUFFER_SIZE];
     cbLeftToReceive = dataLength;
     
     do
     { 
      int iiGet, iiRecd;
      
      iiGet = (cbLeftToReceive<RECV_BUFFER_SIZE) ? cbLeftToReceive : RECV_BUFFER_SIZE ;
      iiRecd = recv(hSocket, (char *)recdData, iiGet, 0);
      
      // test for errors and get out if they occurred
      if ( iiRecd == SOCKET_ERROR || iiRecd == 0 )
      {
       int iErr = ::GetLastError();
       TRACE( "GetFileFromRemoteSite returned a socket error while getting chunked file data\n"
        "\tNumber of Bytes received (zero means connection was closed) = %d\n"
        "\tGetLastError = %d\n", iiRecd, iErr );
       
       /* you should handle the error here */
       
       bRet = FALSE;
       goto PreReturnCleanup;
      }

    /************************* 
      un-comment this code and put a breakpoint here to prove to yourself that sockets can return fewer bytes than requested
       
       if ( iiGet != iiRecd )
       {
       int ii=0;
       }   
    ***************************/
      
      // good data was retrieved, so accumulate it with already-received data
      
      destFile.Write( recdData, iiRecd); // Write it
      cbLeftToReceive -= iiRecd;
      
     }
     while ( cbLeftToReceive > 0 );
     
     
    PreReturnCleanup:  // labelled "goto" destination
     
     // free allocated memory
     // if we got here from a goto that skipped allocation, delete of NULL pointer
     // is permissible under C++ standard and is harmless
     delete[] recdData;  

     if ( bFileIsOpen )
      destFile.Close(); // only close file if it's open (open might have failed above)

     return bRet;
    }

     

    发表于 @ 2006年06月29日 15:28:00|评论(loading...)|编辑

    新一篇: 目录的打包和解包 | 旧一篇: Windows界面——使用Custom Draw优雅的实现ListCtrl的重绘

    评论

    #BlackHan 发表于2006-06-29 16:15:00  IP: 218.242.140.*
    数据缓冲建议在栈上分配内存,可避免内存释放问题(不需要delete):
    byte recdData [RECV_BUFFER_SIZE];

    destFile 应该并不需要close ,离开此函数体会调用析构,内部会调用Close,

    所以 PreReturnCleanup:标签是不需要的,自然也不需要goto了。

    以上这么做是C++的习惯(以免java,C#程序员总是攻击我们落后)
    #BlackHan 发表于2006-06-29 16:17:00  IP: 218.242.140.*
    另外,此程序有严重的安全性隐患:
    读取的 dataLength 一定要检查值是否在预期的范围内,否则任何人可以简单的填满你的硬盘。
    #一雨田 发表于2006-06-29 17:01:00  IP: 211.154.109.*
    一段代码只能说提供了一个思路,如果要真正应用到自己的项目上去,当然还需要细心的修改和测试。
    我贴出代码只是帮助一些人能直接看到效果,不是说让你直接使用到自己的项目上去。
    #一雨田 发表于2006-06-29 16:57:00  IP: 211.154.109.*
    呵呵,安全性的隐患确实没想过。像这种:

    BOOL bRet = TRUE;

    AllocSomeRes();

    if (SomeErrorOccur0())
    {
    bRet = FALSE;
    goto FINAL;
    }

    if (SomeErrorOccur1())
    {
    bRet = FALSE;
    goto FINAL;
    }

    FINAL:

    DeAllocSomething();

    return bRet;

    个人觉得很适合使用goto.

    #BlackHan 发表于2006-06-29 17:17:00  IP: 218.242.140.*
    上面的 goto 用法当然没错。
    只不过能够不用goto,而且代码更干净 为什么非要用呢
    #BlackHan 发表于2006-06-29 17:30:00  IP: 218.242.140.*
    DeAllocSomething() 这一类的操作不应由程序员来做,应该由语言本身(比如自动的对象释放)

    并且说实话,函数中过多的返回结果判断实际上是有害的,增加了代码的"不好的气味",并且不利于维护,这就是为什么微软要把
    系统API的返回状态由返回值进化成异常来处理(Win32--> .Net)


    因为返回值来判断操作是否失败毕竟是20多年前技术的习惯。
    我知道很多C++程序员工作了很多年,仍然从来不使用(或者不会使用抛出异常) 这类C++技术,返回值固然方便,但结果就是将错误检查分散在代码的各个地方,是一件很令人遗憾的习惯。


    #gg 发表于2006-08-08 11:34:00  IP: 59.42.102.*
    dingding
    #Bill 发表于2006-08-22 14:38:00  IP: 58.240.178.*
    Learning
    #性感MM 发表于2006-08-22 20:37:00  IP: 219.137.68.*
    ^_^,我也知道
    发表评论  


    登录
    Csdn Blog version 3.1a
    Copyright © 一雨田