4. TCP粘包与分包处理
任务目标
TCP是流式协议,多次send的数据可能被合并(粘包),一次send的数据也可能被拆分(分包)。本案例设计一个4字节长度前缀 + 消息体的简单协议,在Qt中实现客户端发送、服务端接收并正确还原每条完整消息。
协议定义
| 4字节(大端,消息体长度) | 消息体(UTF-8文本) |
核心实现
// MessageProtocol.h
#ifndef MESSAGEPROTOCOL_H
#define MESSAGEPROTOCOL_H
#include <QByteArray>
#include <QDataStream>
#include <QtEndian>
class MessageProtocol {
public:
// 将消息封装为 [长度(4字节) + 消息体]
static QByteArray pack(const QByteArray& payload) {
QByteArray frame;
QDataStream out(&frame, QIODevice::WriteOnly);
out.setByteOrder(QDataStream::BigEndian);
out << quint32(payload.size());
frame.append(payload);
return frame;
}
// 从缓冲区中解析出所有完整消息,返回剩余未处理的数据
static QByteArray unpack(QByteArray& buffer, QList<QByteArray>& messages) {
messages.clear();
while (buffer.size() >= 4) {
quint32 len = qFromBigEndian<quint32>(
reinterpret_cast<const uchar*>(buffer.constData()));
if (buffer.size() < 4 + len) {
break; // 数据不完整,等待更多数据
}
messages.append(buffer.mid(4, len));
buffer.remove(0, 4 + len);
}
return buffer; // 剩余不完整数据
}
};
#endif // MESSAGEPROTOCOL_H
服务端(接收并解析)
// TcpServer.h
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QTcpServer>
#include <QTcpSocket>
#include <QList>
#include "MessageProtocol.h"
class TcpServer : public QTcpServer {
Q_OBJECT
public:
explicit TcpServer(QObject* parent = nullptr) : QTcpServer(parent) {}
protected:
void incomingConnection(qintptr socketDescriptor) override {
QTcpSocket* socket = new QTcpSocket(this);
socket->setSocketDescriptor(socketDescriptor);
connect(socket, &QTcpSocket::readyRead, this, [this, socket]() {
QByteArray buffer = socket->readAll();
m_buffers[socket].append(buffer);
QList<QByteArray> messages;
MessageProtocol::unpack(m_buffers[socket], messages);
for (const QByteArray& msg : messages) {
qInfo() << "收到完整消息:" << QString::fromUtf8(msg);
}
});
connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater);
}
private:
QMap<QTcpSocket*, QByteArray> m_buffers;
};
#endif // TCPSERVER_H
客户端(发送多条消息)
// main.cpp(含客户端与服务端启动)
#include <QCoreApplication>
#include <QTcpSocket>
#include <QThread>
#include "TcpServer.h"
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
// 启动服务端
TcpServer server;
if (!server.listen(QHostAddress::LocalHost, 8888)) {
qFatal("服务端监听失败");
return 1;
}
qInfo() << "服务端已启动,端口 8888";
// 客户端连接并连续发送3条消息(模拟粘包/分包场景)
QTcpSocket client;
client.connectToHost(QHostAddress::LocalHost, 8888);
if (!client.waitForConnected(1000)) {
qFatal("客户端连接失败");
return 1;
}
// 一次性写入3条完整帧(模拟粘包)
QByteArray allData;
allData.append(MessageProtocol::pack("Hello"));
allData.append(MessageProtocol::pack("World"));
allData.append(MessageProtocol::pack("Qt TCP"));
client.write(allData);
// 模拟分包:分两次发送一条消息
QByteArray partial = MessageProtocol::pack("Split Message");
client.write(partial.left(5)); // 先发前5字节
client.waitForBytesWritten(100);
client.write(partial.mid(5)); // 再发剩余部分
client.flush();
// 等待服务端处理
QThread::msleep(500);
client.disconnectFromHost();
return 0;
}
运行与验证
- 在
.pro文件中添加QT += network。 - 编译并运行程序,观察控制台输出。
预期输出(服务端依次打印4条完整消息):
收到完整消息: Hello
收到完整消息: World
收到完整消息: Qt TCP
收到完整消息: Split Message
判定标准:若控制台按顺序输出以上4条消息且无乱码、无缺失,说明粘包(前3条合并发送)和分包(第4条分两次发送)均被正确处理。若未实现长度前缀协议,直接按流读取将无法区分消息边界。
关键说明
pack()使用QDataStream写入4字节大端长度,保证跨平台一致性。unpack()维护每个连接的接收缓冲区,循环解析出所有完整帧,剩余不足4字节或长度不足的数据保留在缓冲区等待下次readyRead。- 服务端使用
QMap<QTcpSocket*, QByteArray>为每个连接独立缓存,避免多连接数据混淆。