TCP 粘包
TCP 是面向连接的、安全的流式传输协议。所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,因此会产生粘包现象。那什么是粘包呢?
举个栗子:
A 与 B 进行 TCP 通信,A 先后给 B 发送了一个 100 字节和 200 字节的数据包,理性状态下 B 应该收到两个数据包,分别是 100 字节和 200 字节,事实并非如此!B 可能直接收到 300 字节;或者 B 先收到 50 字节,再收到 250 字节;或者先收到 150 字节,再收到 150 字节;或者先收到 50 字节,再收到 100 字节,再收到 150 字节。等等情况~
对于以上描述的现象我们将其称为 TCP 粘包问题。其实这种说法并不准确,因为 TCP 本身就是面向连接的流式传输协议,它这个协议就是这样。多个数据包粘连在一起或者一个数据包被拆分为多个数据包,这个应该是程序员需要解决的问题,而与 TCP 无关。
什么时候需要处理 TCP 粘包问题?
1、如果发送方发送的多组数据是同一块数据的不同部分,比如说一个文件被分成多个部分发送,这时就不需要处理粘包现象。
2、如果多个分组毫不相关,甚至是并列关系,那么这个时候就需要处理粘包现象。
如何处理?
一、发送端
对于发送端造成的粘包,可以通过关闭 Nagle 算法来解决,
QTcpSocket tcpSocket;
tcpSocket.setSocketOption(QAbstractSocket::LowDelayOption, 1);
下面是一个连续发送数据的示例:
for (int i = 0; i < 10; i++) {
QString msg = "123";
m_client->write(msg.toLocal8Bit());
m_client->flush();
QThread::msleep(10000);
}
我发现即使关闭了 Nagle 算法,接收端也是会等全部 write 后才响应。但添加 flush 后,接收端可以正常收到。所以此方法我没有测试成功,可能和 Qt 的缓存机制有关系。
二、接收端
接收端是没有办法来处理粘包问题的,因为它没办法知道边界。
三、应用层
其实 TCP 粘包的本质问题在于无法区分包的界限,那么可以采用以下三种方式:
1、固定长度:
服务端和客户端规定固定长度的缓冲区,当消息数据长度不足时,使用规定的填充字符进行填充。弊端:增加不必要的数据传输。
2、使用标识符:
每条数据有固定的格式(开始符、结束符).弊端:消息体中不能包含标识符。
3、数据包的头部增加数据包长度字段:
发送每条数据时,将数据的长度一并发送。该方法为处理粘包半包的常用方法。
发送端:
#pragma pack(push, 1)
typedef struct
{
int len; // 长度
char data[1024]; // 包体
} NetPacket ;
#pragma pack(pop)
for (int i = 0; i < 2; i++) {
NetPacket p1;
p1.data = "123";
p1.len = p1.data.size();
m_client->write((char *)&p1, sizeof(int) + p1.len);
}
接收端:
char buf[1024]; // 接收数据的缓冲区
char tmpBuf[1024]; // 存放包体
int offset = 0; // 偏移
int n = socket->bytesAvailable();
socket->read(buf, n);
int len;
memcpy(&len, buf, sizeof(int));
offset += sizeof(int);
memcpy(tmpBuf, buf + offset, len);
offset += len;
qDebug() << QByteArray(tmpBuf, len);
memcpy(&len, buf, sizeof(int));
offset += sizeof(int);
memcpy(tmpBuf, buf + offset, len);
offset += len;
qDebug() << QByteArray(tmpBuf, len);