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

运行与验证

  1. 在.pro文件中添加 QT += network。
  2. 编译并运行程序,观察控制台输出。

预期输出(服务端依次打印4条完整消息):

收到完整消息: Hello
收到完整消息: World
收到完整消息: Qt TCP
收到完整消息: Split Message

判定标准:若控制台按顺序输出以上4条消息且无乱码、无缺失,说明粘包(前3条合并发送)和分包(第4条分两次发送)均被正确处理。若未实现长度前缀协议,直接按流读取将无法区分消息边界。

关键说明

  • pack() 使用 QDataStream 写入4字节大端长度,保证跨平台一致性。
  • unpack() 维护每个连接的接收缓冲区,循环解析出所有完整帧,剩余不足4字节或长度不足的数据保留在缓冲区等待下次 readyRead。
  • 服务端使用 QMap<QTcpSocket*, QByteArray> 为每个连接独立缓存,避免多连接数据混淆。