zoukankan      html  css  js  c++  java
  • 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不能这样做,我尝试过,他无法正确取到数据,遇到之类就不往下进行了。



    既然说到这里了,我们不得不说下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

  • 相关阅读:
    npm配置国内源方法
    数据库—事务—隔离级别
    Mybatis—日志
    Mybatis—动态 SQL
    Mybatis—mapper.xml配置文件
    declare命令
    shell杂项
    流程控制语句
    第一篇博客
    Linux 命令[2]:mkdir
  • 原文地址:https://www.cnblogs.com/findumars/p/5641848.html
Copyright © 2011-2022 走看看