• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            每天早晨叫醒你的不是鬧鐘,而是夢想

              C++博客 :: 首頁 :: 聯系 :: 聚合  :: 管理
              62 Posts :: 0 Stories :: 5 Comments :: 0 Trackbacks

            常用鏈接

            留言簿(1)

            我參與的團隊

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            作者:fengge8ylf  博客:http://blog.csdn.net/fengge8ylf

            對于基于TCP開發的通訊程序,有個很重要的問題需要解決,就是封包和拆包.自從我從事網絡通訊編程工作以來(大概有三年的時間了),我一直在思索和改進封包和拆包的方法.下面就針對這個問題談談我的想法,拋磚引玉.若有不對,不妥之處,懇求大家指正.在此先謝過大家了.

            一.為什么基于TCP的通訊程序需要進行封包和拆包.

            TCP是個"流"協議,所謂流,就是沒有界限的一串數據.大家可以想想河里的流水,是連成一片的,其間是沒有分界線的.但一般通訊程序開發是需要定義一個個相互獨立的數據包的,比如用于登陸的數據包,用于注銷的數據包.由于TCP"流"的特性以及網絡狀況,在進行數據傳輸時會出現以下幾種情況.
            假設我們連續調用兩次send分別發送兩段數據data1和data2,在接收端有以下幾種接收情況(當然不止這幾種情況,這里只列出了有代表性的情況).
            A.先接收到data1,然后接收到data2.
            B.先接收到data1的部分數據,然后接收到data1余下的部分以及data2的全部.
            C.先接收到了data1的全部數據和data2的部分數據,然后接收到了data2的余下的數據.
            D.一次性接收到了data1和data2的全部數據.

            對于A這種情況正是我們需要的,不再做討論.對于B,C,D的情況就是大家經常說的"粘包",就需要我們把接收到的數據進行拆包,拆成一個個獨立的數據包.為了拆包就必須在發送端進行封包.
             另:對于UDP來說就不存在拆包的問題,因為UDP是個"數據包"協議,也就是兩段數據間是有界限的,在接收端要么接收不到數據要么就是接收一個完整的一段數據,不會少接收也不會多接收.

            二.為什么會出現B.C.D的情況.
             "粘包"可發生在發送端也可發生在接收端.
             1.由Nagle算法造成的發送端的粘包:Nagle算法是一種改善網絡傳輸效率的算法.簡單的說,當我們提交一段數據給TCP發送時,TCP并不立刻發送此段數據,而是等待一小段時間,看看在等待期間是否還有要發送的數據,若有則會一次把這兩段數據發送出去.這是對Nagle算法一個簡單的解釋,詳細的請看相關書籍.象C和D的情況就有可能是Nagle算法造成的.
                    2.接收端接收不及時造成的接收端粘包:TCP會把接收到的數據存在自己的緩沖區中,然后通知應用層取數據.當應用層由于某些原因不能及時的把TCP的數據取出來,就會造成TCP緩沖區中存放了幾段數據.

            三.怎樣封包和拆包.
               最初遇到"粘包"的問題時,我是通過在兩次send之間調用sleep來休眠一小段時間來解決.這個解決方法的缺點是顯而易見的,使傳輸效率大大降低,而且也并不可靠.后來就是通過應答的方式來解決,盡管在大多數時候是可行的,但是不能解決象B的那種情況,而且采用應答方式增加了通訊量,加重了網絡負荷(但是象FTP等協議采用的就是應答方式).再后來就是對數據包進行封包和拆包的操作.
                封包:
             封包就是給一段數據加上包頭,這樣一來數據包就分為包頭和包體兩部分內容了(以后講過濾非法包時封包會加入"包尾"內容).包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變量,其他的結構體成員可根據需要自己定義.根據包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包.
                對于拆包目前我最常用的是以下兩種方式.
                1.動態緩沖區暫存方式.之所以說緩沖區是動態的是因為當需要緩沖的數據長度超出緩沖區的長度時會增大緩沖區長度.
                大概過程描述如下:
                A,為每一個連接動態分配一個緩沖區,同時把此緩沖區和SOCKET關聯,常用的是通過結構體關聯.
                B,當接收到數據時首先把此段數據存放在緩沖區中.
                C,判斷緩存區中的數據長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
                D,根據包頭數據解析出里面代表包體長度的變量.
                E,判斷緩存區中除包頭外的數據長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
                F,取出整個數據包.這里的"取"的意思是不光從緩沖區中拷貝出數據包,而且要把此數據包從緩存區中刪除掉.刪除的辦法就是把此包后面的數據移動到緩沖區的起始地址.

                這種方法有兩個缺點.1.為每個連接動態分配一個緩沖區增大了內存的使用.2.有三個地方需要拷貝數據,一個地方是把數據存放在緩沖區,一個地方是把完整的數據包從緩沖區取出來,一個地方是把數據包從緩沖區中刪除.這種拆包的改進方法會解決和完善部分缺點.

                下面給出相關代碼.

            先看包頭結構定義

             

             1 #pragma pack(push,1//開始定義數據包, 采用字節對齊方式
             2 /*----------------------包頭---------------------*/
             3 typedef struct tagPACKAGEHEAD
             4 {
             5  BYTE Version;
             6  WORD Command;
             7  WORD nDataLen;//包體的長度
             8 }PACKAGE_HEAD;
             9 #pragma pack(pop) //結束定義數據包, 恢復原來對齊方式 
            10 
            11 然后看存放數據和""數據函數.
            12 
            13 /*****************************************************************************
            14 Description:添加數據到緩存
            15 Input:pBuff[in]-待添加的數據;nLen[in]-待添加數據長度
            16 Return: 如果當前緩沖區沒有足夠的空間存放pBuff則返回FALSE;否則返回TRUE。
            17 ******************************************************************************/
            18 BOOL CDataBufferPool::AddBuff( char *pBuff, int nLen )
            19 {
            20  m_cs.Lock();///臨界區鎖
            21 
            22  if ( nLen < 0 )
            23  {
            24   m_cs.Unlock();
            25   return FALSE;
            26  }
            27 
            28  if ( nLen <= GetFreeSize() )///判斷剩余空間是否足夠存放nLen長的數據
            29  {
            30   memcpy(m_pBuff + m_nOffset, pBuff, nLen);
            31   m_nOffset += nLen;
            32  }
            33  else///若不夠則擴充原有的空間
            34  { 
            35   char *= m_pBuff;
            36   m_nSize += nLen*2;//每次增長2*nLen
            37   m_pBuff = new char[m_nSize];
            38   memcpy(m_pBuff,p,m_nOffset);
            39   delete []p;
            40   memcpy(m_pBuff + m_nOffset, pBuff, nLen);
            41   m_nOffset += nLen;
            42   m_cs.Unlock();
            43   return FALSE;
            44  }
            45  m_cs.Unlock();
            46  return TRUE;
            47 }
            48 
            49  
            50 
            51 /*****************************************************************************
            52 Description:獲取一個完整的包
            53 Input:Buf[out]-獲取到的數據;nLen[out]-獲取到的數據長度
            54 Return: 1、當前緩沖區不夠一個包頭的數據 2、當前緩沖區不夠一個包體的數據
            55 ******************************************************************************/
            56 
            57 int  CDataBufferPool::GetFullPacket( char *Buf, int& nLen )
            58 {
            59  m_cs.Lock();
            60 
            61  if ( m_nOffset < m_PacketHeadLen )//當前緩沖區不夠一個包頭的數據
            62  {
            63   m_cs.Unlock();
            64   return 1;
            65  }
            66  PACKAGE_HEAD *= (PACKAGE_HEAD *)m_pBuff;
            67  if( (m_nOffset-m_PacketHeadLen) < (int)p->nDataLen )//當前緩沖區不夠一個包體的數據
            68  {
            69   m_cs.Unlock();
            70   return 2;
            71  }
            72  //判斷包的合法性
            73 /* int IsIntegrallity = ValidatePackIntegrality(p);
            74  if( IsIntegrallity != 0 )
            75  {
            76   m_cs.Unlock();
            77   return IsIntegrallity;
            78  }
            79 */
            80  nLen = m_PacketHeadLen+p->nDataLen;
            81  memcpy( Buf, m_pBuff, nLen );
            82  m_nOffset -= nLen;
            83  memcpy( m_pBuff, m_pBuff+nLen, m_nOffset );
            84 
            85  m_cs.Unlock();
            86  return 0;
            87 }
            88 
            89 


                前面提到過這種方法的缺點.下面給出一個改進辦法, 即采用環形緩沖.但是這種改進方法還是不能解決第一個缺點以及第一個數據拷貝,只能解決第三個地方的數據拷貝(這個地方是拷貝數據最多的地方).第2種拆包方式會解決這兩個問題.
             環形緩沖實現方案是定義兩個指針,分別指向有效數據的頭和尾.在存放數據和刪除數據時只是進行頭尾指針的移動.
             用代碼來說明.注:下面的代碼是采用一個開源的游戲服務器的代碼,我對此代碼有所修改.
             

              1 int CCircularBufferPool::PutData(TCHAR *pData, int len)
              2 {
              3  if( len <= 0 ) 
              4   return 1;
              5 
              6  EnterCriticalSection(&m_cs);
              7  while (IsOverFlowCondition(len))///判斷緩沖區剩余空間是否夠存放len長的數據
              8  {
              9   BufferResize(len);///若不夠,則擴充緩沖區.
             10  }
             11 
             12  if (IsIndexOverFlow(len))///判斷"尾"指針的位置.
             13  {
             14   int FirstCopyLen = m_iBufSize-m_iTailPos;
             15   int SecondCopyLen = len - FirstCopyLen;
             16   CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
             17   if (SecondCopyLen)
             18   {
             19    CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
             20    m_iTailPos = SecondCopyLen;
             21   }
             22   else 
             23    m_iTailPos = 0;
             24  }
             25  else
             26  {
             27   CopyMemory(m_pBuffer+m_iTailPos, pData, len);
             28   m_iTailPos += len;
             29  }
             30 
             31  LeaveCriticalSection(&m_cs);
             32  return 0;
             33 
             34 
             35 }
             36 
             37 
             38 void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
             39 {
             40  if (len < m_iBufSize-m_iHeadPos)
             41  {
             42   CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
             43   if(Delete==true)
             44    m_iHeadPos += len;
             45  }
             46  else
             47  {
             48   int fc, sc;
             49   fc = m_iBufSize-m_iHeadPos;
             50   sc = len - fc;
             51   CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
             52   if (sc) CopyMemory(pData+fc, m_pBuffer, sc);
             53   if(Delete==true)
             54    m_iHeadPos = sc;
             55   if(m_iHeadPos >= m_iBufSize)
             56    m_iHeadPos = 0;
             57 
             58  }
             59 }
             60 
             61 //
             62 //進行自定義包的解析
             63 //
             64 int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
             65 {
             66  EnterCriticalSection(&m_cs);
             67  if( GetValidCount() < m_PacketHeadLen )//當前緩沖區不夠一個包頭的數據
             68  {
             69   LeaveCriticalSection(&m_cs);
             70   return 1;
             71  }
             72 
             73  GetData(Buf,m_PacketHeadLen,false);
             74  PACKAGE_HEAD *= (PACKAGE_HEAD *)Buf;
             75  if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//當前緩沖區不夠一個包體的數據
             76  {
             77   LeaveCriticalSection(&m_cs);
             78   return 2;
             79  }
             80 
             81  //判斷包的合法性
             82  int IsIntegrallity = ValidatePackIntegrality(p);
             83  if( IsIntegrallity != 0 )
             84  {
             85   LeaveCriticalSection(&m_cs);
             86   return IsIntegrallity;
             87  }
             88  
             89  GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
             90  nLen = m_PacketHeadLen+p->nDataLen;
             91 
             92  LeaveCriticalSection(&m_cs);
             93 
             94 
             95  return 0;
             96 }
             97 
             98  2.利用底層的緩沖區來進行拆包
             99  由于TCP也維護了一個緩沖區,所以我們完全可以利用TCP的緩沖區來緩存我們的數據,這樣一來就不需要為每一個連接分配一個緩沖區了.另一方面我們知道recv或者wsarecv都有一個參數,用來表示我們要接收多長長度的數據.利用這兩個條件我們就可以對第一種方法進行優化了.
            100       對于阻塞SOCKET來說,我們可以利用一個循環來接收包頭長度的數據,然后解析出代表包體長度的那個變量,再用一個循環來接收包體長度的數據.
            101  相關代碼如下:
            102    
            103  char PackageHead[1024];
            104  char PackageContext[1024*20];
            105 
            106  int len;
            107  PACKAGE_HEAD *pPackageHead;
            108  while( m_bClose == false )
            109  {
            110   memset(PackageHead,0,sizeof(PACKAGE_HEAD));
            111   len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
            112   if( len == SOCKET_ERROR )
            113   {
            114       break;
            115   }
            116   if(len == 0)
            117   {
            118       break;
            119   }
            120   pPackageHead = (PACKAGE_HEAD *)PackageHead;
            121   memset(PackageContext,0,sizeof(PackageContext));
            122   if(pPackageHead->nDataLen>0)
            123   {
            124    len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
            125   }
            126         }
            127 
            128  m_TcpSock是一個封裝了SOCKET的類的變量,其中的ReceiveSize用于接收一定長度的數據,直到接收了一定長度的數據或者網絡出錯才返回.
            129  
            130 int winSocket::ReceiveSize( char* strData, int iLen )
            131 {
            132  if( strData == NULL )
            133   return ERR_BADPARAM;
            134  char *= strData;
            135  int len = iLen;
            136  int ret = 0;
            137  int returnlen = 0;
            138  while( len > 0)
            139  {
            140   ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
            141   if ( ret == SOCKET_ERROR || ret == 0 )
            142   {
            143    
            144    return ret;
            145   }
            146 
            147   len -= ret;
            148   returnlen += ret;
            149  }
            150  
            151  return returnlen;
            152 }
            153 
            154 


             

             對于非阻塞的SOCKET,比如完成端口,我們可以提交接收包頭長度的數據的請求,當GetQueuedCompletionStatus返回時,我們判斷接收的數據長度是否等于包頭長度,若等于,則提交接收包體長度的數據的請求,若不等于則提交接收剩余數據的請求.當接收包體時,采用類似的方法.
             下面給出相關代碼.

             1 enum IOType 
             2 {
             3  IOInitialize,
             4  IORead,
             5  IOWrite,
             6  IOIdle
             7 };
             8 
             9 class OVERLAPPEDPLUS 
            10 {
            11 public:
            12  OVERLAPPED   m_ol;
            13  IOType    m_ioType;
            14  bool         m_bIsPackageHead;//當前接收的數據是否是包頭數據。
            15 
            16  int          m_count;
            17  WSABUF       m_wsaBuffer;
            18  int          m_RecvPos;
            19  char         m_Buffer[1024*8];//此緩沖要盡可能大
            20 
            21  OVERLAPPEDPLUS(IOType ioType) {
            22   ZeroMemory(thissizeof(OVERLAPPEDPLUS));
            23   m_ioType = ioType;
            24  }
            25 };
            26 
            27 

             

            接收連接后發出的第一個請求,請求接收包頭大小的數據.
             

             1 OVERLAPPEDPLUS *pOverlappedPlus =  new OVERLAPPEDPLUS;
             2  pOverlappedPlus->m_wsaBuffer.buf =  pOverlappedPlus->m_Buffer;
             3  pOverlappedPlus->m_wsaBuffer.len =  PACKAGE_HEAD_LEN;///包頭的長度
             4  pOverlappedPlus->m_bIsPackageHead = true;
             5  pOverlappedPlus->m_RecvPos = 0;
             6  pOverlappedPlus->m_ioType = IORead;
             7 
             8 
             9  DWORD RecvBytes;
            10      DWORD Flags;
            11  Flags = 0;
            12  if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), 1&RecvBytes, &Flags,
            13   &pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)
            14  {
            15   if (WSAGetLastError() != ERROR_IO_PENDING)
            16   {
            17    delete pOverlappedPlus;
            18   }
            19   else
            20   {
            21    ///相關的錯誤處理
            22 
            23   }
            24  }
            25  else
            26  {
            27   ///相關的錯誤處理
            28 
            29  }
            30 
            31 


            在GetQueuedCompletionStatus所在的函數中.

             1 if( pOverlapPlus->m_ioType== IORead)
             2  { 
             3   if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )
             4   {
             5    if( pOverlapPlus->m_bIsPackageHead == true )///接收到的是包頭。
             6    {
             7     PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer);
             8 
             9     if(pThis->IsLegalityPackageHead(pPackageHead)==false)///判斷是否是合法的包
            10     {
            11      closesocket(lpClientContext->m_Socket);
            12      continue;
            13     }
            14 
            15     pOverlapPlus->m_bIsPackageHead = false;
            16     pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;
            17     pOverlapPlus->m_RecvPos += dwIoSize;
            18     pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
            19 
            20    }
            21    else///接收到的是包體
            22    {
            23 
            24      
            25 
            26    pOverlapPlus->m_RecvPos += dwIoSize;
            27    ///這時pOverlapPlus->m_Buffer里就存放了一個完整的數據包,長度為pOverlapPlus->m_RecvPos
            28    
            29 
            30    ///繼續請求 請求下一個數據包的包頭
            31    pOverlapPlus->m_wsaBuffer.buf =  pOverlapPlus->m_Buffer;
            32    memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));
            33    pOverlapPlus->m_wsaBuffer.len =  PACKAGE_HEAD_LEN;
            34    pOverlapPlus->m_bIsPackageHead = true;
            35    pOverlapPlus->m_RecvPos = 0;
            36      
            37    }
            38   }
            39   else///接收的數據還不完整
            40   {
            41    pOverlapPlus->m_wsaBuffer.len -= dwIoSize;
            42    pOverlapPlus->m_RecvPos += dwIoSize;
            43    pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
            44   }
            45   pOverlapPlus->m_ioType = IORead;
            46   state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), 1&RecvBytes, &Flags,
            47     &pOverlapPlus->m_ol, NULL);
            48   if ( state == SOCKET_ERROR)
            49   {
            50    if(WSAGetLastError() != ERROR_IO_PENDING)
            51    {
            52        
            53        //關閉套接字 釋放相應資源
            54      continue;
            55    }
            56   }
            57    
            58  }
            59 
            60 


             三:如何判斷包的合法性.
            判斷包的合法性可以結合下面兩種方式來判斷.但是想100%的判定出非法包,只能通過信息安全中的知識來判定了,對這種方法這里不做闡述.
            1.通過包頭的結構來判斷包的合法性.
             最初的時候我是根據包頭來判斷包的合法性,比如判斷Command是否超出命令范圍,nDataLen是否大于最大包的長度.但是這種方法無法過濾掉非法包,當出現非法包時我們唯一能做的就是斷開連接,或許這也是最好的處理辦法.
             我們可以給一個完整的包加上開始和結束標志,標志可以是個整數,也可以是一串字符串.以第一種拆包方式為例來說明.當要拆一個完整包時我們先從緩沖區有效數據頭指針地址搜索包的開始標志,搜索到后并且當前數據夠一個包頭數據,則判斷開始標志和包頭是否合法,若合法則根據代表數據長度的變量的值定位到包尾,判斷包尾標志是否與我們定義的一致,若一致則這個包是合法的包.若有一項不一致則繼續尋找下個包的開始標志,并把下個合法包的前面的數據全部舍棄.
            2.通過邏輯層來判斷包的合法性.
             當取出一個合法的包時,我們還要根據當前數據處理的邏輯來判斷包的合法性.比如說在登陸成功后的某段時間服務器又收到了同一個客戶端的登陸包,那我們就可以判斷這個包是非法的,簡單處理就是斷開連接.

            posted on 2011-05-13 13:26 沛沛 閱讀(851) 評論(1)  編輯 收藏 引用 所屬分類: 網絡

            Feedback

            # re: 封包和拆包 2011-05-14 09:22 K.V
            看到了。有用到但研究不深,Mark。  回復  更多評論
              

            国产毛片久久久久久国产毛片 | 午夜精品久久久久久中宇| 久久伊人亚洲AV无码网站| 久久只有这精品99| 久久综合久久久| 亚洲Av无码国产情品久久| 久久久无码精品亚洲日韩蜜臀浪潮| 久久久久亚洲AV无码永不| 久久精品国产亚洲7777| 亚洲精品乱码久久久久久久久久久久 | 99久久精品费精品国产 | 久久人人爽人人爽人人爽| 国产精品久久久久久久久免费 | 亚洲国产精品无码久久98| 国产精品嫩草影院久久| 久久人人爽人人爽人人AV| 无码国内精品久久人妻麻豆按摩 | 无码精品久久一区二区三区| 久久国产免费观看精品3| 中文字幕无码久久精品青草 | 亚洲精品乱码久久久久久蜜桃不卡| 亚洲国产精品久久久久婷婷软件| 久久久久久久综合狠狠综合| 精品国产热久久久福利| 久久婷婷五月综合国产尤物app | 久久天堂电影网| 久久av无码专区亚洲av桃花岛| 欧美一级久久久久久久大片| 99久久99久久精品国产片果冻| 久久精品天天中文字幕人妻| 99久久精品免费看国产一区二区三区 | 久久久精品视频免费观看| 久久中文骚妇内射| 亚洲AV日韩精品久久久久久久| 久久这里的只有是精品23| 久久精品18| 国产三级精品久久| 久久久久亚洲爆乳少妇无| 久久久WWW免费人成精品| 久久中文精品无码中文字幕| 精品久久久久中文字|