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

这两天用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来输入的,这货使用起来有好也有坏,好处是写入与读取很方便,坏处是他的大小不是我们所想的那

样,很另类,看下面例子:

  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. out << ushort(0) << ushort(0) << m_clientID;
  8. //回到文件开头,插入真实的数值
  9. out.device()->seek(0);
  10. ushort len = (ushort)(sendByte.size());
  11. ushort type_id = 0;
  12. out << type_id << len;
  13. m_tcpClient->write(sendByte);

大体的封包就像上面那样,我们来看主要的粘包代码:
   
先看.h里面一些基本数据变量的声明:

  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中处理粘包的代码:

  1. //接收消息
  2. void ClientThread::slot_readmesg()
  3. {
  4. //缓冲区没有数据,直接无视
  5. if( m_tcpClient->bytesAvailable() <= 0 )
  6. {
  7. return;
  8. }
  9. //临时获得从缓存区取出来的数据,但是不确定每次取出来的是多少。
  10. QByteArray buffer;
  11. //如果是信号readyRead触发的,使用readAll时会一次把这一次可用的数据全总读取出来
  12. //所以使用while(m_tcpClient->bytesAvailable())意义不大,其实只执行一次。
  13. buffer = m_tcpClient->readAll();
  14. //上次缓存加上这次数据
  15. /**
  16. 上面有讲到混包的三种情况,数据A、B,他们过来时有可能是A+B、B表示A包+B包中一部分数据,
  17. 然后是B包剩下的数据,或者是A、A+B表示A包一部分数据,然后是A包剩下的数据与B包组合。
  18. 这个时候,我们解析时肯定会残留下一部分数据,并且这部分数据对于下一包会有效,所以我们
  19. 要和下一包组合起来。
  20. */
  21. m_buffer.append(buffer);
  22. ushort type_id, mesg_len;
  23. int totalLen = m_buffer.size();
  24. while( totalLen )
  25. {
  26. //与QDataStream绑定,方便操作。
  27. QDataStream packet(m_buffer);
  28. packet.setByteOrder(QDataStream::BigEndian);
  29. //不够包头的数据直接就不处理。
  30. if( totalLen < MINSIZE )
  31. {
  32. break;
  33. }
  34. packet >> type_id >> mesg_len;
  35. //如果不够长度等够了在来解析
  36. if( totalLen < mesg_len )
  37. {
  38. break;
  39. }
  40. //数据足够多,且满足我们定义的包头的几种类型
  41. switch(type_id)
  42. {
  43. case MSG_TYPE_ID:
  44. break;
  45. case MSG_TYPE_FILE_START:
  46. {
  47. packet >> m_fileName;
  48. }
  49. break;
  50. case MSG_TYPE_FILE_SENDING:
  51. {
  52. QByteArray tmpdata;
  53. packet >> tmpdata;
  54. //这里我把所有的数据都缓存在内存中,因为我们传输的文件不大,最大才几M;
  55. //大家可以这里收到一个完整的数据包,就往文件里面写入,即使保存。
  56. m_recvData.append(tmpdata);
  57. //这个可以最后拿来校验文件是否传完,或者是否传的完整。
  58. m_checkSize += tmpdata.size();
  59. //打印提示,或者可以连到进度条上面。
  60. emit sig_displayMesg(QString("recv: %1").arg(m_checkSize));
  61. }
  62. break;
  63. case MSG_TYPE_FILE_END:
  64. {
  65. packet >> m_DataSize;
  66. saveImage();
  67. clearData();
  68. }
  69. break;
  70. default:
  71. break;
  72. }
  73. //缓存多余的数据
  74. buffer = m_buffer.right(totalLen - mesg_len);
  75. //更新长度
  76. totalLen = buffer.size();
  77. //更新多余数据
  78. m_buffer = buffer;
  79. }
  80. }

上面的思想和使用正常的平台socket收发一样,如果直接使用socket的API,那里这里就更简单了,解析出数据长度后,就使用数据长度循环去取数据,

直到数据长度变成0,在Qt中使用QDataStream封装QByteArray不能这样做,我尝试过,他无法正确取到数据,遇到\0之类就不往下进行了。

既然说到这里了,我们不得不说下QTcpSokcet在Qt多线程中的使用,Qt的多线程让我又爱又恨,有多时候用起来真不方便。下面直接看下代码:

  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. m_tcpClient->connectToHost(m_addr, m_port);
  11. //默认让其等待3秒吧,反正在线程中连接,又不会卡主界面。
  12. if( m_tcpClient->waitForConnected() )
  13. {
  14. qDebug() << "connect is ok";
  15. }
  16. else
  17. {
  18. qDebug() << "connect is fail";
  19. delete m_tcpClient;
  20. m_tcpClient = NULL;
  21. return ;
  22. }
  23. connect(m_tcpClient, SIGNAL(readyRead()), this, SLOT(slot_readmesg()));
  24. connect(m_tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), this,
  25. SLOT(slot_errors(QAbstractSocket::SocketError)));
  26. }
  27. m_checkSize = 0;
  28. m_DataSize = 0;
  29. m_recvData = "";
  30. //连接成功...
  31. if( m_firstConnect )
  32. {
  33. QByteArray sendByte;
  34. QDataStream out(&sendByte, QIODevice::WriteOnly);
  35. out.setVersion(QDataStream::Qt_5_3);
  36. out.setByteOrder(QDataStream::BigEndian);
  37. //占位符
  38. out << ushort(0) << ushort(0) << m_clientID;
  39. //加到文件开头
  40. out.device()->seek(0);
  41. ushort len = (ushort)(sendByte.size());
  42. ushort type_id = 0;
  43. out << type_id << len;
  44. m_tcpClient->write(sendByte);
  45. m_firstConnect = false;
  46. emit sig_displayMesg(QString("send: %1 %2 %3").arg(type_id).arg(len).arg(QString(m_clientID)));
  47. //qDebug() <<"sendData: " << type_id << " " << len << " " << IDNum << " " << sizeof(sendByte);
  48. }
  49. //不加这个,自动把m_tcpClient析构了,服务端收不到消息。
  50. exec();
  51. }

对于Qt中信号与槽连接,有好几种方式,大家去看看,对于在线程中貌似最好用Qt::DirectConnection的连接,不过看Qt帮助文档,在多线程中默认

的连接方式Qt::AutoConnection表现的和Qt::DirectConnection是一个样的。

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

QTcpSocket 及 TCP粘包分析的更多相关文章

  1. (经典)tcp粘包分析

    转载自csdn:http://blog.csdn.net/zhangxinrun/article/details/6721495 这两天看csdn有一些关于socket粘包,socket缓冲区设置的问 ...

  2. 【转】TCP粘包分析

    一 .两个简单概念长连接与短连接: 1.长连接     Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收. 2.短连接     Client方与Server每 ...

  3. [网络协议]TCP粘包分析

    关于socket粘包,socket缓冲区设置的问题,记录一下: 一 .两个简单概念长连接与短连接: 长连接     Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送 ...

  4. TCP粘包分析与处理

    http://www.cnblogs.com/liyux/p/5594423.html http://www.cnblogs.com/liyux/p/5603826.html

  5. tcp粘包问题(封包)

    tcp粘包分析     http://blog.csdn.net/zhangxinrun/article/details/6721495 解决TCP网络传输“粘包”问题(经典)       http: ...

  6. TCP 粘包及其解决方案(zz)

    首先,我们回顾一下 TCP 和 UDP 的头部信息: 具体说明看:http://www.cnblogs.com/aomi/p/7776582.html 我们知道,TCP 和 UDP 是 TCP/IP ...

  7. 转,关于TCP粘包问题的一些思路

    TCP粘包分析 一 .两个简单概念长连接与短连接:1.长连接 Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收. 2.短连接 Client方与Server每进 ...

  8. TCP粘包问题分析和解决(全)

    TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送 ...

  9. 【转载】TCP粘包问题分析和解决(全)

    TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送 ...

随机推荐

  1. AJAX验证数据库内容并显示在页面

    功能实现: 在jsp页面中填写文本框内容,光标离开文本框,在本页面的相应地方获取数据库中改值所对应的其他数据. servlet: request.setCharacterEncoding(" ...

  2. ThinkPHP框架二

    ThinkPHP笔记二 1.1 TP框架的配置文件 在TP框架中,所有的配置文件都是自动加载的,加载的顺序:惯例配置<应用配置<调试配置<模块配置<动态配置 1. 惯例配置(T ...

  3. 整理:GET与POST的区别

    1.根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的. (1).所谓安全的意味着该操作用于获取信息而非修改信息.换句话说,GET 请求一般不应产生副作用.就是说,它仅仅是获取资源信息,就 ...

  4. web开发(Java&Jquery)实现验证码

    1. Ajax Fancy Capcha 一个支持 Ajax 又很炫的 jQuery Captcha 插件,它使用了很人性化的验证机制. ​ from : http://www.webdesignbe ...

  5. WIN_2003_SP2.iso VMware 不能进行网络访问的处理

    1.打开IE发现不能上网的问题 ping命令 发现确实是网络不通: 2.关闭虚拟机设置网络适配器 3.ping命令验证是否设置成功 4.打开IE访问百度

  6. [置顶] Firefox OS 学习——简单了解知识

    什么是Firefox OS ? Firefox OS 是一个为网页设计而生的能编译和独立的手机网页操作系统,我们相信在接下来的时代,网页应用将充满整个新兴操作设备,这也为当前许多网页开发者不需要太多的 ...

  7. java logger级别

    typedef enum android_LogPriority { ANDROID_LOG_UNKNOWN = 0, ANDROID_LOG_DEFAULT,    /* only for SetM ...

  8. Java报错--Unsupported major.minor version 52.0

    遇到一个Java相关的报错: ... java.lang.UnsupportedClassVersionError: ... : Unsupported major.minor version 52. ...

  9. oracle归档日志管理

    归档日志(Archive Log)是非活动的重做日志备份.通过使用归档日志,可以保留所有重做历史记录,当数据库处于ARCHIVELOG模式并进行日志切换式,后台进程ARCH会将重做日志的内容保存到归档 ...

  10. mvn profile 深层次目录打参数核心配置

    <build> <resources> <resource> <directory>src/main/resources</directory&g ...