天天看點

QTcpSocket 及 TCP粘包分析

一、長連接配接與短連接配接

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來輸入的,這貨使用起來有好也有壞,好處是寫入與讀取很友善,壞處是他的大小不是我們所想的那樣,很另類,看下面例子:

QByteArray sendByte;
QDataStream out(&sendByte, QIODevice::WriteOnly);
//out.setVersion(QDataStream::Qt_5_3);
//設定大端模式,C++、JAVA中都是使用的大端,一般隻有linux的嵌入式使用的小端
out.setByteOrder(QDataStream::BigEndian);

//占位符,這裡必須要先這樣占位,然後後續讀算出整體長度後在插入
out << ushort() << ushort() << m_clientID;
//回到檔案開頭,插入真實的數值
out.device()->seek();
ushort len = (ushort)(sendByte.size());
ushort type_id = ;
out << type_id << len;

m_tcpClient->write(sendByte);
           

大體的封包就像上面那樣,

我們來看主要的粘包代碼:先看.h裡面一些基本資料變量的聲明:

//圖檔名字
QByteArray m_fileName;
//接收到的資料
QByteArray m_recvData;
//實際圖檔資料大小
qint64 m_DataSize;
//接收圖檔資料大小
qint64 m_checkSize;
//緩存上一次或多次的未處理的資料
//這個用來處理,重新粘包
QByteArray m_buffer;
           

上面最主要的地方是那個m_buffer,他在粘包過程中起決定性的作用。

下面來看.cpp中處理粘包的代碼:

//接收消息
void ClientThread::slot_readmesg()
{
    //緩沖區沒有資料,直接無視
    if( m_tcpClient->bytesAvailable() <=  )
    {
        return;
    }

    //臨時獲得從緩存區取出來的資料,但是不确定每次取出來的是多少。
    QByteArray buffer;
    //如果是信号readyRead觸發的,使用readAll時會一次把這一次可用的資料全總讀取出來
    //是以使用while(m_tcpClient->bytesAvailable())意義不大,其實隻執行一次。
    buffer = m_tcpClient->readAll();


    //上次緩存加上這次資料
    /*
   上面有講到混包的三種情況,資料A、B,他們過來時有可能是A+B、B表示A包+B包中一部分資料,
   然後是B包剩下的資料,或者是A、A+B表示A包一部分資料,然後是A包剩下的資料與B包組合。
   這個時候,我們解析時肯定會殘留下一部分資料,并且這部分資料對于下一包會有效,是以我們
   要和下一包組合起來。
  */
    m_buffer.append(buffer);

    ushort type_id, mesg_len;

    int totalLen = m_buffer.size();

    while( totalLen )
    {
        //與QDataStream綁定,友善操作。
        QDataStream packet(m_buffer);
        packet.setByteOrder(QDataStream::BigEndian);

        //不夠標頭的資料直接就不處理。
        if( totalLen < MINSIZE )
        {
            break;
        }

        packet >> type_id >> mesg_len;

        //如果不夠長度等夠了在來解析
        if( totalLen < mesg_len )
        {
            break;
        }   

        //資料足夠多,且滿足我們定義的標頭的幾種類型
        switch(type_id)
        {
        case MSG_TYPE_ID:
            break;

        case MSG_TYPE_FILE_START:
        {
            packet >> m_fileName;
        }
            break;         

        case MSG_TYPE_FILE_SENDING:
        {
            QByteArray tmpdata;
            packet >> tmpdata;
            //這裡我把所有的資料都緩存在記憶體中,因為我們傳輸的檔案不大,最大才幾M;
            //大家可以這裡收到一個完整的資料包,就往檔案裡面寫入,即使儲存。
            m_recvData.append(tmpdata);
            //這個可以最後拿來校驗檔案是否傳完,或者是否傳的完整。
            m_checkSize += tmpdata.size();
            //列印提示,或者可以連到進度條上面。
            emit sig_displayMesg(QString("recv: %1").arg(m_checkSize));
        }
            break;         

        case MSG_TYPE_FILE_END:
        {
            packet >> m_DataSize;
            saveImage();
            clearData();
        }
            break;

        default:
            break;
        }

        //緩存多餘的資料
        buffer = m_buffer.right(totalLen - mesg_len);

        //更新長度
        totalLen = buffer.size();

        //更新多餘資料
        m_buffer = buffer;

    }
}
           

上面的思想和使用正常的平台socket收發一樣,如果直接使用socket的API,那裡這裡就更簡單了,解析出資料長度後,就使用資料長度循環去取資料,直到資料長度變成0,在Qt中使用QDataStream封裝QByteArray不能這樣做,我嘗試過,他無法正确取到資料,遇到\0之類就不往下進行了。

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

//Qt中在QThread類的run()函數裡面定義或調用的一切都認為是線上程中運作的,
//非run()裡面調用或定義的依然在GUI主線程中。
void ClientThread::run()
{
    qDebug() << "thread id: " << currentThreadId();
    if( m_tcpClient == NULL )
    {
        //要想qtcpsocket是多線程,必須在run裡面定義
        m_tcpClient = new TcpClient();

        m_tcpClient->connectToHost(m_addr, m_port);     

        //預設讓其等待3秒吧,反正線上程中連接配接,又不會卡主界面。
        if( m_tcpClient->waitForConnected() )
        {
            qDebug() << "connect is ok";
        }
        else
        {
            qDebug() << "connect is fail";         

            delete m_tcpClient;            

            m_tcpClient = NULL;

            return ;
        }
        connect(m_tcpClient, SIGNAL(readyRead()), this, SLOT(slot_readmesg()));
        connect(m_tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), 
                this, SLOT(slot_errors(QAbstractSocket::SocketError)));
    }

    m_checkSize = ;    
    m_DataSize = ;    
    m_recvData = "";    

    //連接配接成功...
    if( m_firstConnect )
    {
        QByteArray sendByte;
        QDataStream out(&sendByte, QIODevice::WriteOnly);
        out.setVersion(QDataStream::Qt_5_3);
        out.setByteOrder(QDataStream::BigEndian);

        //占位符
        out << ushort() << ushort() << m_clientID;
        //加到檔案開頭
        out.device()->seek();
        ushort len = (ushort)(sendByte.size());
        ushort type_id = ;
        out << type_id << len;        

        m_tcpClient->write(sendByte);
        m_firstConnect = false;

        emit sig_displayMesg(QString("send: %1 %2 %3").arg(type_id).arg(len).arg(QString(m_clientID)));
        //qDebug() <<"sendData: " << type_id << " " << len << " " << IDNum << " " << sizeof(sendByte);
    }

    //不加這個,自動把m_tcpClient析構了,服務端收不到消息。
    exec();
}
           

對于Qt中信号與槽連接配接,有好幾種方式,大家去看看,對于線上程中貌似最好用Qt::DirectConnection的連接配接,不過看Qt幫助文檔,在多線程中預設的連接配接方式Qt::AutoConnection表現的和Qt::DirectConnection是一個樣的。

轉載自:http://www.aiuxian.com/article/p-1732805.html