QTcpSocket 及 TCP粘包分析


~~~~我的生活,我的點點滴滴!!

 

這兩天用Qt簡單的實現一個tcp多線程client,在此記錄下知識。

 

 

一、長連接與短連接

 

1、長連接

   Client方與Server方先建立通訊連接,連接建立后不斷開, 然后再進行報文發送和接收。
   

2、短連接

   Client方與Server每進行一次報文收發交易時才進行通訊連接,交易完畢后立即斷開連接。此種方式常用於一點對多點通訊,比如多個Client

   連接一個Server。

   

 

二、什么時候需要考慮粘包問題?

 

1、如果利用tcp每次發送數據,就與對方建立連接,然后雙方發送完一段數據后,就關閉連接,這樣就不會出現粘包問題(因為只有一種包結構,

   類似於http協議)。關閉連接主要要雙方都發送close連接(參考tcp關閉協議)。如:A需要發送一段字符串給B,那么A與B建立連接,然后發

   送雙方都默認好的協議字符如"hello give me sth abour yourself",然后B收到報文后,就將緩沖區數據接收,然后關閉連接,這樣粘包問題

   不用考慮到,因為大家都知道是發送一段字符。

   

2、如果發送數據無結構,如文件傳輸,這樣發送方只管發送,接收方只管接收存儲就ok,也不用考慮粘包。

3、如果雙方建立連接,需要在連接后一段時間內發送不同結構數據,如連接后,有好幾種結構:

   a、"hello give me abour your message"

   b、"Don't give me  abour your message"

   這樣的話,如果發送方連續發送兩個這樣的包出去,接收方一次接收可能會是"hello give me abour your messageDon't give me  abour

your message",這樣接收方就傻眼了,到底應該怎么分了?因為沒有協議規定怎么拆分這段字符串,所以要處理好分包,需要雙方組織一個比較

好的包結構,一般會在頭上加上消息類型,消息長度等以確保正常接收。




三、粘包出現原因

 

     粘包只可能出現在流傳輸中,TCP是基於流傳輸的,而UDP是不會出現粘包,因為他是基於報文的,也就是說UDP發送端調用幾次write,

接收端必須調用相同次數的read讀完,他每次最多只能讀取一個報文,報文與報文是不會合並的,如果緩沖區小於報文長度,則多出來的部

分會被丟掉。TCP不同了,他會合並消息,並且以不確定方式合並,這樣就需要我們去粘包處理了,TCP造成粘包主要原因:

 

    1、發送端需要等緩沖區滿了才發送出去,造成粘包。

    2、接收方不及時接收緩沖區的包,造成多個包一起接收。


解決方法:

    為了避免粘包現象,可采取以下幾種措施:

    1、對於發送方引起的粘包現象,用戶可通過編程設置來避免,TCP提供了強制數據立即傳送的操作指令push,TCP軟件收到該操作指令后,

       就立即將本段數據發送出去,而不必等待發送緩沖區滿;

    2、是對於接收方引起的粘包,則可通過優化程序設計、精簡接收進程工作量、提高接收進程優先級等措施,使其及時接收數據,從而盡

       量避免出現粘包現象;

    3、是由接收方控制,將一包數據按結構字段,人為控制分多次接收,然后合並,通過這種手段來避免粘包。

一般大多數都是使用第三種方法,自己定義包協議格式,然后人為粘包,那么我們就需要知道TCP發送時,大概會有哪幾種包情況產生:

    1、先接收到data1,然后接收到data2。 這是我們希望的,但是往往不是這樣的。

    2、先接收到data1的部分數據,然后接收到data1余下的部分以及data2的全部。

    3、先接收到了data1的全部數據和data2的部分數據,然后接收到了data2的余下的數據。

    4、一次性接收到了data1和data2的全部數據。

上面就是主要的幾種情況,一般就是這幾種,對於2、3、4就需要我們粘包處理了。



四、怎樣封包和拆包

    最初遇到"粘包"的問題時,我是通過在兩次send之間調用sleep來休眠一小段時間來解決。這個解決方法的缺點是顯而易見的,使傳輸效率大

大降低,而且也並不可靠。后來就是通過應答的方式來解決,盡管在大多數時候是可行的,但是不能解決象2的那種情況,而且采用應答方式增加了

通訊量,加重了網絡負荷..再后來就是對數據包進行封包和拆包的操作。


1、封包

 

  封包就是給一段數據加上包頭,這樣一來數據包就分為包頭和包體兩部分內容了(以后講過濾非法包時封包會加入"包尾"內容)。包頭其實上是個

大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變量,其他的結構體成員可根據需要自己定義。根據包頭長度固定以

及包頭中含有包體長度的變量就能正確的拆分出一個完整的數據包。

 

 

2、拆包

 

   利用底層的緩沖區來進行拆包,由於TCP也維護了一個緩沖區,所以我們完全可以利用TCP的緩沖區來緩存我們的數據,這樣一來就不需要為每一個

連接分配一個緩沖區了,對於利用緩沖區來拆包,也就是循環不停的接收包頭給出的數據,直到收夠為止,這就是一個完整的TCP包。下面我們來講

解利用Qt的QTcpSocket來進行拆包、粘包的過程。


   首先,我們定義包體結構是利用QDataStream來輸入的,這貨使用起來有好也有壞,好處是寫入與讀取很方便,壞處是他的大小不是我們所想的那

樣,很另類,看下面例子:

      

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. QByteArray sendByte;  
  2. QDataStream out(&sendByte, QIODevice::WriteOnly);  
  3. //out.setVersion(QDataStream::Qt_5_3);  
  4. //設置大端模式,C++、JAVA中都是使用的大端,一般只有linux的嵌入式使用的小端  
  5. out.setByteOrder(QDataStream::BigEndian);  
  6.   
  7.   
  8. //占位符,這里必須要先這樣占位,然后后續讀算出整體長度后在插入  
  9. out << ushort(0) << ushort(0) << m_clientID;  
  10. //回到文件開頭,插入真實的數值  
  11. out.device()->seek(0);  
  12. ushort len = (ushort)(sendByte.size());  
  13. ushort type_id = 0;  
  14. out << type_id << len;  
  15.   
  16.   
  17. m_tcpClient->write(sendByte);  


大體的封包就像上面那樣,我們來看主要的粘包代碼:
   
先看.h里面一些基本數據變量的聲明:

 

 

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //圖片名字  
  2. QByteArray m_fileName;  
  3. //接收到的數據  
  4. QByteArray m_recvData;  
  5. //實際圖片數據大小  
  6. qint64 m_DataSize;  
  7. //接收圖片數據大小  
  8. qint64 m_checkSize;  
  9. //緩存上一次或多次的未處理的數據  
  10. //這個用來處理,重新粘包  
  11. QByteArray m_buffer;  


上面最主要的地方是那個m_buffer,他在粘包過程中起決定性的作用。
   
下面來看.cpp中處理粘包的代碼:

 

 

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //接收消息  
  2.     void ClientThread::slot_readmesg()  
  3.     {  
  4.         //緩沖區沒有數據,直接無視  
  5.         if( m_tcpClient->bytesAvailable() <= 0 )  
  6.         {  
  7.             return;  
  8.         }  
  9.           
  10.         //臨時獲得從緩存區取出來的數據,但是不確定每次取出來的是多少。  
  11.         QByteArray buffer;  
  12.         //如果是信號readyRead觸發的,使用readAll時會一次把這一次可用的數據全總讀取出來  
  13.         //所以使用while(m_tcpClient->bytesAvailable())意義不大,其實只執行一次。  
  14.         buffer = m_tcpClient->readAll();  
  15.   
  16.   
  17.         //上次緩存加上這次數據  
  18.         /** 
  19.             上面有講到混包的三種情況,數據A、B,他們過來時有可能是A+B、B表示A包+B包中一部分數據, 
  20.             然后是B包剩下的數據,或者是A、A+B表示A包一部分數據,然后是A包剩下的數據與B包組合。 
  21.             這個時候,我們解析時肯定會殘留下一部分數據,並且這部分數據對於下一包會有效,所以我們 
  22.             要和下一包組合起來。 
  23.         */  
  24.         m_buffer.append(buffer);  
  25.   
  26.   
  27.         ushort type_id, mesg_len;  
  28.   
  29.   
  30.         int totalLen = m_buffer.size();  
  31.   
  32.   
  33.         while( totalLen )  
  34.         {  
  35.             //與QDataStream綁定,方便操作。  
  36.             QDataStream packet(m_buffer);  
  37.             packet.setByteOrder(QDataStream::BigEndian);  
  38.   
  39.   
  40.             //不夠包頭的數據直接就不處理。  
  41.             if( totalLen < MINSIZE )  
  42.             {  
  43.                 break;  
  44.             }  
  45.   
  46.   
  47.             packet >> type_id >> mesg_len;  
  48.   
  49.   
  50.             //如果不夠長度等夠了在來解析  
  51.             if( totalLen < mesg_len )  
  52.             {  
  53.                 break;  
  54.             }  
  55.   
  56.   
  57.             //數據足夠多,且滿足我們定義的包頭的幾種類型  
  58.             switch(type_id)  
  59.             {  
  60.                 case MSG_TYPE_ID:  
  61.                 break;  
  62.   
  63.   
  64.                 case MSG_TYPE_FILE_START:  
  65.                 {  
  66.                     packet >> m_fileName;  
  67.                 }  
  68.                 break;  
  69.   
  70.   
  71.                 case MSG_TYPE_FILE_SENDING:  
  72.                 {  
  73.                     QByteArray tmpdata;  
  74.                     packet >> tmpdata;  
  75.                     //這里我把所有的數據都緩存在內存中,因為我們傳輸的文件不大,最大才幾M;  
  76.                     //大家可以這里收到一個完整的數據包,就往文件里面寫入,即使保存。  
  77.                     m_recvData.append(tmpdata);  
  78.                     //這個可以最后拿來校驗文件是否傳完,或者是否傳的完整。  
  79.                     m_checkSize += tmpdata.size();  
  80.                     //打印提示,或者可以連到進度條上面。  
  81.                     emit sig_displayMesg(QString("recv: %1").arg(m_checkSize));  
  82.                 }  
  83.                 break;  
  84.   
  85.   
  86.                 case MSG_TYPE_FILE_END:  
  87.                 {  
  88.                     packet >> m_DataSize;  
  89.                     saveImage();  
  90.                     clearData();  
  91.                 }  
  92.                     break;  
  93.   
  94.   
  95.                 default:  
  96.                 break;  
  97.             }  
  98.   
  99.   
  100.             //緩存多余的數據  
  101.             buffer = m_buffer.right(totalLen - mesg_len);  
  102.   
  103.   
  104.             //更新長度  
  105.             totalLen = buffer.size();  
  106.   
  107.   
  108.             //更新多余數據  
  109.             m_buffer = buffer;  
  110.   
  111.   
  112.         }  
  113.     }  

 


上面的思想和使用正常的平台socket收發一樣,如果直接使用socket的API,那里這里就更簡單了,解析出數據長度后,就使用數據長度循環去取數據,

直到數據長度變成0,在Qt中使用QDataStream封裝QByteArray不能這樣做,我嘗試過,他無法正確取到數據,遇到\0之類就不往下進行了。



既然說到這里了,我們不得不說下QTcpSokcet在Qt多線程中的使用,Qt的多線程讓我又愛又恨,有多時候用起來真不方便。下面直接看下代碼:

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //Qt中在QThread類的run()函數里面定義或調用的一切都認為是在線程中運行的,  
  2. //非run()里面調用或定義的依然在GUI主線程中。  
  3. void ClientThread::run()  
  4. {  
  5.     qDebug() << "thread id: " << currentThreadId();  
  6.     if( m_tcpClient == NULL )  
  7.     {  
  8.         //要想qtcpsocket是多線程,必須在run里面定義  
  9.         m_tcpClient = new TcpClient();  
  10.   
  11.   
  12.         m_tcpClient->connectToHost(m_addr, m_port);  
  13.   
  14.   
  15.         //默認讓其等待3秒吧,反正在線程中連接,又不會卡主界面。  
  16.         if( m_tcpClient->waitForConnected() )  
  17.         {  
  18.             qDebug() << "connect is ok";  
  19.         }  
  20.         else  
  21.         {  
  22.             qDebug() << "connect is fail";  
  23.   
  24.   
  25.             delete m_tcpClient;  
  26.   
  27.   
  28.             m_tcpClient = NULL;  
  29.   
  30.   
  31.             return ;  
  32.         }  
  33.         connect(m_tcpClient, SIGNAL(readyRead()), this, SLOT(slot_readmesg()));  
  34.         connect(m_tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), this,  
  35.                              SLOT(slot_errors(QAbstractSocket::SocketError)));  
  36.     }  
  37.   
  38.   
  39.     m_checkSize = 0;  
  40.   
  41.   
  42.     m_DataSize = 0;  
  43.   
  44.   
  45.     m_recvData = "";  
  46.   
  47.   
  48.     //連接成功...  
  49.     if( m_firstConnect )  
  50.     {  
  51.         QByteArray sendByte;  
  52.         QDataStream out(&sendByte, QIODevice::WriteOnly);  
  53.         out.setVersion(QDataStream::Qt_5_3);  
  54.         out.setByteOrder(QDataStream::BigEndian);  
  55.   
  56.   
  57.         //占位符  
  58.         out << ushort(0) << ushort(0) << m_clientID;  
  59.         //加到文件開頭  
  60.         out.device()->seek(0);  
  61.         ushort len = (ushort)(sendByte.size());  
  62.         ushort type_id = 0;  
  63.         out << type_id << len;  
  64.   
  65.   
  66.         m_tcpClient->write(sendByte);  
  67.         m_firstConnect = false;  
  68.   
  69.   
  70.         emit sig_displayMesg(QString("send: %1 %2 %3").arg(type_id).arg(len).arg(QString(m_clientID)));  
  71.         //qDebug() <<"sendData: " << type_id << " " << len << " " << IDNum << " " << sizeof(sendByte);  
  72.     }  
  73.   
  74.   
  75.     //不加這個,自動把m_tcpClient析構了,服務端收不到消息。  
  76.     exec();  
  77. }  

 

對於Qt中信號與槽連接,有好幾種方式,大家去看看,對於在線程中貌似最好用Qt::DirectConnection的連接,不過看Qt幫助文檔,在多線程中默認

的連接方式Qt::AutoConnection表現的和Qt::DirectConnection是一個樣的。

http://blog.csdn.net/ac_huang/article/details/40791767


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM