Qt网络编程——处理TCP粘包

时间:2024-03-07 14:37:41

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);