QT TCPsocket 封包 粘包分析
来源:互联网 发布:邮件服务器25端口不通 编辑:程序博客网 时间:2024/05/19 17:23
这两天用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来输入的,这货使用起来有好也有坏,好处是写入与读取很方便,坏处是他的大小不是我们所想的那
样,很另类,看下面例子:
01
QByteArray sendByte;
02
QDataStream out(&sendByte, QIODevice::WriteOnly);
03
//out.setVersion(QDataStream::Qt_5_3);
04
//设置大端模式,C++、JAVA中都是使用的大端,一般只有linux的嵌入式使用的小端
05
out.setByteOrder(QDataStream::BigEndian);
06
07
08
//占位符,这里必须要先这样占位,然后后续读算出整体长度后在插入
09
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里面一些基本数据变量的声明:
01
//图片名字
02
QByteArray m_fileName;
03
//接收到的数据
04
QByteArray m_recvData;
05
//实际图片数据大小
06
qint64 m_DataSize;
07
//接收图片数据大小
08
qint64 m_checkSize;
09
//缓存上一次或多次的未处理的数据
10
//这个用来处理,重新粘包
11
QByteArray m_buffer;
上面最主要的地方是那个m_buffer,他在粘包过程中起决定性的作用。
下面来看.cpp中处理粘包的代码:
001
//接收消息
002
void
ClientThread::slot_readmesg()
003
{
004
//缓冲区没有数据,直接无视
005
if
( m_tcpClient->bytesAvailable() <= 0 )
006
{
007
return
;
008
}
009
010
//临时获得从缓存区取出来的数据,但是不确定每次取出来的是多少。
011
QByteArray buffer;
012
//如果是信号readyRead触发的,使用readAll时会一次把这一次可用的数据全总读取出来
013
//所以使用while(m_tcpClient->bytesAvailable())意义不大,其实只执行一次。
014
buffer = m_tcpClient->readAll();
015
016
017
//上次缓存加上这次数据
018
/**
019
上面有讲到混包的三种情况,数据A、B,他们过来时有可能是A+B、B表示A包+B包中一部分数据,
020
然后是B包剩下的数据,或者是A、A+B表示A包一部分数据,然后是A包剩下的数据与B包组合。
021
这个时候,我们解析时肯定会残留下一部分数据,并且这部分数据对于下一包会有效,所以我们
022
要和下一包组合起来。
023
*/
024
m_buffer.append(buffer);
025
026
027
ushort type_id, mesg_len;
028
029
030
int
totalLen = m_buffer.size();
031
032
033
while
( totalLen )
034
{
035
//与QDataStream绑定,方便操作。
036
QDataStream packet(m_buffer);
037
packet.setByteOrder(QDataStream::BigEndian);
038
039
040
//不够包头的数据直接就不处理。
041
if
( totalLen < MINSIZE )
042
{
043
break
;
044
}
045
046
047
packet >> type_id >> mesg_len;
048
049
050
//如果不够长度等够了在来解析
051
if
( totalLen < mesg_len )
052
{
053
break
;
054
}
055
056
057
//数据足够多,且满足我们定义的包头的几种类型
058
switch
(type_id)
059
{
060
case
MSG_TYPE_ID:
061
break
;
062
063
064
case
MSG_TYPE_FILE_START:
065
{
066
packet >> m_fileName;
067
}
068
break
;
069
070
071
case
MSG_TYPE_FILE_SENDING:
072
{
073
QByteArray tmpdata;
074
packet >> tmpdata;
075
//这里我把所有的数据都缓存在内存中,因为我们传输的文件不大,最大才几M;
076
//大家可以这里收到一个完整的数据包,就往文件里面写入,即使保存。
077
m_recvData.append(tmpdata);
078
//这个可以最后拿来校验文件是否传完,或者是否传的完整。
079
m_checkSize += tmpdata.size();
080
//打印提示,或者可以连到进度条上面。
081
emit sig_displayMesg(QString(
"recv: %1"
).arg(m_checkSize));
082
}
083
break
;
084
085
086
case
MSG_TYPE_FILE_END:
087
{
088
packet >> m_DataSize;
089
saveImage();
090
clearData();
091
}
092
break
;
093
094
095
default
:
096
break
;
097
}
098
099
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的多线程让我又爱又恨,有多时候用起来真不方便。下面直接看下代码:
01
//Qt中在QThread类的run()函数里面定义或调用的一切都认为是在线程中运行的,
02
//非run()里面调用或定义的依然在GUI主线程中。
03
void
ClientThread::run()
04
{
05
qDebug() <<
"thread id: "
<< currentThreadId();
06
if
( m_tcpClient == NULL )
07
{
08
//要想qtcpsocket是多线程,必须在run里面定义
09
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是一个样的。
- QT TCPsocket 封包 粘包分析
- Qt的TCPsocket通信
- 关于TCP封包、粘包、半包
- 关于TCP封包、粘包、半包
- 关于TCP封包、粘包、半包
- 关于TCP封包、粘包、半包
- 关于TCP封包、粘包、半包
- Socket 粘包 封包 拆包
- Socket封包、拆包、粘包
- QT TcpSocket 传送结构体
- QT tcpsocket 发送/接收数据
- Tcpsocket
- TCPSocket
- Ethereal 抓包、封包內容分析、查看明码教学
- 网络之 TCP封包、粘包、半包
- C++ Qt多线程 TcpSocket服务器实例
- C++ Qt多线程 TcpSocket服务器实例
- Qt文件封包
- 1080. MOOC期终成绩 (未拿满分)
- yum 安装 mongo 3.6.0
- 初学者---Android 多渠道打包
- 关于define宏定义
- 5.Java中的访问控制权限
- QT TCPsocket 封包 粘包分析
- 单源最短路径
- 机房系统——导出excel表
- Rpackage【ggplot2】
- mysql如果数据不存在,则插入新数据,否则更新的实现方法
- (对象类作为参数的方法)
- Java Web 服务器的消息推送 几种方案
- 异步IO、协程、yield from
- opencv 2.4+ c++ 边缘梯度计算