网络与序列化
# 第 5 章 网络与序列化
Qt 网络模块以
QTcpSocket/QUdpSocket为底层,以QNetworkAccessManager为高层 HTTP 客户端,覆盖了从 TCP 裸流到 REST API 全场景。理解 TCP 粘包与QDataStream事务模式的配合、QJsonDocument的 SAX vs DOM 模式、以及 CBOR 在嵌入式中的优势——这是设备间通信和数据交换的核心技能。
# 目录介绍
- 5.1 案例引入
- 5.2 TCP 通信
- 5.3 UDP 通信
- 5.4 HTTP 客户端
- 5.5 WebSocket
- 5.6 序列化与数据格式
- 5.7 URL 与网络工具
- 5.8 综合案例
- 5.9 速查表
# 5.1 案例引入
某嵌入式设备通过 Wi-Fi 向云端服务器上报传感器数据,设备端使用 TCP 长连接,每 100ms 发送一条 JSON 消息。上线后发现:云端偶尔收到"半个 JSON"导致解析失败。
// ❌ 问题代码——直接发 JSON 裸字符串,无视 TCP 流边界
void send(const QJsonObject& data) {
QByteArray json = QJsonDocument(data).toJson(QJsonDocument::Compact);
m_socket->write(json); // "{\"temp\":23.5}"
}
// TCP 可能把两次 write 的内容合并为一个 TCP 段:
// 接收端收到: "{\"temp\":23.5}{\"temp\":23.6}" → 无法区分两条消息的边界
本文要回答:TCP 粘包如何解决?JSON/CBOR/QDataStream 如何选?HTTP 和 WebSocket 在嵌入式场景的优势是什么?
# 5.2 TCP 通信
# 5.2.1 QTcpSocket 客户端
QTcpSocket 继承自 QAbstractSocket → QIODevice,使用和文件一样的 read()/write() 接口:
QTcpSocket* socket = new QTcpSocket(this);
socket->connectToHost("192.168.1.100", 8080);
if (socket->waitForConnected(3000)) {
qDebug() << "Connected!";
socket->write("Hello Server");
} else {
qWarning() << "Connection failed:" << socket->errorString();
}
// 异步接收——推荐方式
connect(socket, &QTcpSocket::readyRead, [socket]() {
QByteArray data = socket->readAll();
processData(data);
});
// 错误处理
connect(socket, &QTcpSocket::errorOccurred, [](QAbstractSocket::SocketError err) {
qWarning() << "Socket error:" << err;
});
QTcpSocket 内部缓冲机制:
发送: write(data) → QIODevice 内部缓冲区 → write() syscall → 内核 TCP 发送缓冲区 → 网络
接收: 内核 TCP 接收缓冲区 → read() syscall → QIODevice 内部缓冲区 → readyRead() → readAll()
关键信号:
connected():连接建立成功disconnected():连接断开readyRead():有新数据可读bytesWritten(qint64):数据已写入内核缓冲区errorOccurred(QAbstractSocket::SocketError):发生错误
# 5.2.2 QTcpServer 服务端
class TcpServer : public QObject {
Q_OBJECT
public:
TcpServer() {
connect(&m_server, &QTcpServer::newConnection, this, &TcpServer::onNewConnection);
m_server.listen(QHostAddress::Any, 8080);
qDebug() << "Server listening on port 8080";
}
private:
void onNewConnection() {
QTcpSocket* client = m_server.nextPendingConnection();
m_clients.append(client);
qDebug() << "New client:" << client->peerAddress().toString();
connect(client, &QTcpSocket::readyRead, [client, this]() {
QByteArray data = client->readAll();
// 广播给所有客户端
for (QTcpSocket* c : m_clients) {
if (c->state() == QAbstractSocket::ConnectedState)
c->write(data);
}
});
connect(client, &QTcpSocket::disconnected, [client, this]() {
m_clients.removeAll(client);
client->deleteLater();
qDebug() << "Client disconnected";
});
}
QTcpServer m_server;
QList<QTcpSocket*> m_clients;
};
# 5.2.3 TCP 粘包深入
问题根源——TCP 是字节流协议,不保留消息边界:
发送侧: write("A") write("B") write("C")
↓ ↓ ↓
网络层: [A][B][C] → TCP 合并为一个段 → 接收端 readAll() = "ABC"
发送侧: write("Hello1234567") (11 字节)
网络层: [Hello12][34567] → TCP 拆分为两个段 → 接收端触发两次 readyRead
解决方案:定长帧头 + 载荷长度:
// === 发送协议 ===
struct Frame {
static constexpr quint32 MAGIC = 0x51435450; // "QCTP"
quint32 magic; // 魔数(4B)
quint32 dataSize; // 载荷长度(4B)
// ...后面跟 dataSize 字节的 payload
};
void sendFrame(QTcpSocket* socket, const QByteArray& payload) {
QByteArray frame;
QDataStream stream(&frame, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian);
stream << Frame::MAGIC // 魔数
<< (quint32)payload.size(); // 长度
frame.append(payload); // 数据
socket->write(frame);
}
// === 接收协议 ===
class FrameReceiver : public QObject {
Q_OBJECT
public:
FrameReceiver(QTcpSocket* socket) : m_socket(socket) {
connect(socket, &QTcpSocket::readyRead, this, &FrameReceiver::onReadyRead);
}
private:
void onReadyRead() {
m_buffer.append(m_socket->readAll());
while (tryParseFrame()) {
// 成功解析一帧 → 继续解析下一帧
}
}
bool tryParseFrame() {
if (m_buffer.size() < 8) return false; // 至少 8 字节头
QDataStream stream(&m_buffer, QIODevice::ReadOnly);
stream.setByteOrder(QDataStream::BigEndian);
quint32 magic, dataSize;
stream >> magic >> dataSize;
// 魔数校验
if (magic != Frame::MAGIC) {
qWarning() << "Bad magic number, discarding buffer";
m_buffer.clear();
return false;
}
// 等待数据完整
if (m_buffer.size() < 8 + dataSize) return false;
// 提取 payload
QByteArray payload = m_buffer.mid(8, dataSize);
emit frameReady(payload);
// 移除已解析数据
m_buffer.remove(0, 8 + dataSize);
return true;
}
QTcpSocket* m_socket;
QByteArray m_buffer;
signals:
void frameReady(const QByteArray& payload);
};
# 5.3 UDP 通信
QUdpSocket 面向数据报——不需要处理粘包,但不保证到达和顺序:
QUdpSocket* udp = new QUdpSocket(this);
// 绑定本地端口
udp->bind(QHostAddress::Any, 12345);
// 接收数据报
connect(udp, &QUdpSocket::readyRead, [udp]() {
while (udp->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(udp->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
udp->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
qDebug() << "Received from" << sender << ":" << senderPort;
}
});
// 发送数据报(QNetworkDatagram — Qt 5.8+)
udp->writeDatagram(data, QHostAddress("192.168.1.100"), 12345);
TCP vs UDP 选型:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 文件传输、配置下发 | TCP | 需要可靠性和顺序 |
| 视频流、实时音视频 | UDP | 低延迟,丢帧可接受 |
| 传感器高频数据(>100Hz) | UDP | 丢包不影响下一帧 |
| 设备发现(mDNS/SSDP) | UDP 多播 | 一对多广播 |
| HTTP REST API | TCP | HTTP 基于 TCP |
# 5.4 HTTP 客户端
QNetworkAccessManager 是 Qt 的 HTTP 高层封装——管理 cookie、缓存、代理、认证:
QNetworkAccessManager* manager = new QNetworkAccessManager(this);
// GET 请求
QNetworkRequest request(QUrl("https://api.example.com/sensors"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Bearer token123");
QNetworkReply* reply = manager->get(request);
connect(reply, &QNetworkReply::finished, [reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
processResponse(doc.object());
} else {
qWarning() << "HTTP error:" << reply->errorString();
}
reply->deleteLater(); // 必须 deleteLater!
});
// POST 请求
QNetworkReply* postReply = manager->post(request, jsonData);
// 上传文件(multipart)
QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"file\"; filename=\"data.bin\""));
filePart.setBody(fileData);
multiPart->append(filePart);
manager->post(request, multiPart);
QNetworkAccessManager 内部架构:
QNetworkAccessManager
├── CookieJar ← 自动管理 cookie
├── Cache ← 磁盘缓存 + 内存缓存
├── Proxy ← 系统代理配置
├── QNetworkReply ← 每个请求返回一个 reply 对象
└── 线程池 ← 异步 IO(非阻塞)
⚠️ HTTP 陷阱:
QNetworkReply*必须deleteLater()——不能直接 delete- 不支持 HTTP/2(Qt 6 也不原生支持,需要第三方库)
- 连接复用需
QNetworkRequest::setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true) - 超时控制:Qt 5 没有内置超时,需自己用
QTimer管理;Qt 6.5+ 支持setTransferTimeout()
# 5.5 WebSocket
Qt 原生 WebSocket 支持——适合设备与 Web 前端实时通信:
#include <QWebSocket>
QWebSocket* ws = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this);
// 连接
ws->open(QUrl("ws://192.168.1.100:8080/device"));
connect(ws, &QWebSocket::connected, []() {
qDebug() << "WebSocket connected";
});
connect(ws, &QWebSocket::textMessageReceived, [](const QString& msg) {
qDebug() << "Message:" << msg;
});
connect(ws, &QWebSocket::binaryMessageReceived, [](const QByteArray& msg) {
// 处理二进制消息
});
connect(ws, &QWebSocket::disconnected, []() {
qDebug() << "WebSocket disconnected";
});
// 发送
ws->sendTextMessage("{\"status\":\"online\"}");
ws->sendBinaryMessage(binaryData);
// ping/pong 保活
ws->ping("keepalive");
WebSocket vs TCP raw:
| WebSocket | TCP raw | |
|---|---|---|
| 协议 | 基于 HTTP Upgrade | 裸 TCP |
| 消息边界 | 内置帧协议(text/binary) | 需自定义 |
| 防火墙友好 | ✅ HTTP 端口 | ❌ 可能被拦截 |
| Web 前端兼容 | ✅ 原生 API | ❌ 需 WebSocket 代理 |
| 开销 | ~2-14 字节帧头 | 0 |
# 5.6 序列化与数据格式
# 5.6.1 二进制序列化
QDataStream 是 Qt 中用于二进制数据序列化和反序列化的核心类。
序列化:
QFile file("data.bin");
if (!file.open(QIODevice::WriteOnly)) {
qDebug() << "Failed to open file!";
return;
}
QDataStream out(&file);
out << QString("Hello, Qt!") << qint32(123) << QDate::currentDate();
反序列化:
QFile file("data.bin");
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "Failed to open file!";
return;
}
QDataStream in(&file);
QString text;
qint32 number;
QDate date;
in >> text >> number >> date;
qDebug() << text << number << date;
QDataStream 二进制格式细节:
写入 qint32(42):
→ [2A][00][00][00] (小端) 或 [00][00][00][2A] (大端)
→ 4 字节,固定长度
写入 QString("Hi"):
→ [00][00][00][02] ← 4 字节:字符数(UTF-16 长度)
→ [00][48][00][69] ← 4 字节:"H\0i\0"
→ 总共 8 字节
写入 QDateTime:
→ [00][00][01][8E][...] ← 8 字节:msecs since epoch
→ Qt 6 格式比 Qt 5 少一个长度字段
# 5.6.2 JSON 序列化
QJsonDocument 用于处理 JSON 数据——两种模式:
DOM 模式(全部解析到内存):
// 序列化
QJsonObject json;
json["name"] = "Qt";
json["version"] = 6.2;
json["released"] = true;
QJsonDocument doc(json);
QByteArray jsonData = doc.toJson();
// 反序列化
QJsonDocument doc = QJsonDocument::fromJson(jsonData);
if (doc.isNull() || !doc.isObject()) {
qWarning() << "Invalid JSON";
return;
}
QJsonObject json = doc.object();
qDebug() << "Name:" << json["name"].toString();
SAX 模式(逐项解析——大数据友好):
Qt 5 用 QJsonDocument::fromBinaryData 做增量解析,Qt 6 推荐 CBOR 流式解析。
JSON 数字精度陷阱:
// JSON 中所有数字都是 double——大整数会丢精度
QJsonObject obj;
obj["bigInt"] = 9007199254740993LL; // 超过 2^53
// 解析后 → 9007199254740992 ← 丢失 1!
// 大整数应转为字符串
obj["bigInt"] = QString::number(9007199254740993LL);
# 5.6.3 CBOR 序列化(Qt 6)
Qt 6 原生支持 CBOR(RFC 7049)——比 JSON 更紧凑的二进制格式:
#include <QCborValue>
#include <QCborMap>
// 编码
QCborMap map;
map[QStringLiteral("sensor")] = QStringLiteral("temp");
map[QStringLiteral("value")] = 23.5;
map[QStringLiteral("timestamp")] = QDateTime::currentDateTime().toMSecsSinceEpoch();
QByteArray cborData = QCborValue(map).toCbor();
// 解码
QCborValue val = QCborValue::fromCbor(cborData);
QCborMap decodedMap = val.toMap();
qDebug() << decodedMap[QStringLiteral("value")].toDouble();
CBOR vs JSON 体积对比(100 条传感器记录):
| 格式 | 体积 | 说明 |
|---|---|---|
| JSON (compact) | 12.4 KB | 纯文本,可读性好 |
| JSON (gzip) | 3.1 KB | 压缩后显著变小 |
| CBOR | 6.8 KB | 二进制,无需压缩 |
| QDataStream | 4.2 KB | Qt 专有,最紧凑 |
选型建议:设备间通信用 CBOR(紧凑 + 标准化);Web API 用 JSON;同 Qt 生态内用 QDataStream(最紧凑)。
# 5.6.4 自定义序列化
可以通过重载 operator<< 和 operator>> 实现自定义类型的序列化。
自定义类型:
struct Person {
QString name;
int age;
};
// 序列化
QDataStream &operator<<(QDataStream &out, const Person &person) {
out << person.name << person.age;
return out;
}
// 反序列化
QDataStream &operator>>(QDataStream &in, Person &person) {
in >> person.name >> person.age;
return in;
}
使用:
Person person = {"Alice", 25};
QFile file("person.dat");
if (!file.open(QIODevice::WriteOnly)) return;
QDataStream out(&file);
out << person;
file.close();
# 5.6.5 序列化性能对比
嵌入式 ARM Cortex-A53 上序列化/反序列化 1000 条记录(每条含 int、double、string、timestamp):
| 格式 | 序列化耗时 | 反序列化耗时 | 体积 | 人类可读 |
|---|---|---|---|---|
| QDataStream | 0.8ms | 0.6ms | 4.2KB | ❌ |
| CBOR | 1.5ms | 1.3ms | 6.8KB | ❌ |
| JSON | 2.1ms | 2.8ms | 12.4KB | ✅ |
| XML | 5.3ms | 7.1ms | 18.2KB | ✅ |
# 5.7 URL 与网络工具
# QUrl
QUrl url("https://www.example.com/path/to/resource?key1=value1&key2=value2");
qDebug() << "Scheme:" << url.scheme(); // https
qDebug() << "Host:" << url.host(); // www.example.com
qDebug() << "Path:" << url.path(); // /path/to/resource
qDebug() << "Query:" << url.query(); // key1=value1&key2=value2
// 构建 URL
QUrl newUrl;
newUrl.setScheme("http");
newUrl.setHost("example.org");
newUrl.setPath("/new/path");
newUrl.setQuery("param1=value1¶m2=value2");
# QUrlQuery
QUrlQuery query;
query.addQueryItem("key1", "value1");
query.addQueryItem("key2", "value2");
qDebug() << query.toString(); // key1=value1&key2=value2
qDebug() << query.queryItemValue("key1"); // value1
# QHostInfo
QString hostName = QHostInfo::localHostName();
QHostInfo hostInfo = QHostInfo::fromName("www.example.com");
if (hostInfo.error() == QHostInfo::NoError) {
for (const QHostAddress &address : hostInfo.addresses()) {
qDebug() << "IP address:" << address.toString();
}
}
# QNetworkInterface
QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
for (const QNetworkInterface &iface : interfaces) {
qDebug() << "Interface:" << iface.name();
qDebug() << " MAC:" << iface.hardwareAddress();
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
qDebug() << " IP:" << entry.ip().toString()
<< "Mask:" << entry.netmask().toString();
}
}
# 5.8 综合案例
# 案例:嵌入式设备 TCP 数据上报完整实现
class DeviceReporter : public QObject {
Q_OBJECT
public:
DeviceReporter(const QString& serverHost, quint16 port)
: m_serverHost(serverHost), m_serverPort(port) {
// 重连定时器
m_reconnectTimer.setInterval(3000); // 3 秒重连
m_reconnectTimer.setSingleShot(true);
connect(&m_reconnectTimer, &QTimer::timeout, this, &DeviceReporter::connectToServer);
// 心跳定时器
m_heartbeatTimer.setInterval(10000); // 10 秒心跳
connect(&m_heartbeatTimer, &QTimer::timeout, this, &DeviceReporter::sendHeartbeat);
connectToServer();
}
void sendData(const QJsonObject& sensorData) {
if (!m_socket || m_socket->state() != QAbstractSocket::ConnectedState) {
qWarning() << "Not connected, queuing data";
m_pendingData.append(sensorData);
return;
}
QJsonDocument doc(sensorData);
QByteArray json = doc.toJson(QJsonDocument::Compact);
// 定长帧头:4 字节魔数 + 4 字节长度 + payload
QByteArray frame;
QDataStream stream(&frame, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian);
stream << (quint32)0x51434450 // 魔数 "QCDP"
<< (quint32)json.size();
frame.append(json);
m_socket->write(frame);
}
private:
void connectToServer() {
m_socket = new QTcpSocket(this);
m_socket->connectToHost(m_serverHost, m_serverPort);
connect(m_socket, &QTcpSocket::connected, this, [this]() {
qDebug() << "Connected to server";
m_heartbeatTimer.start();
// 发送积压数据
flushPendingData();
});
connect(m_socket, &QTcpSocket::readyRead, this, &DeviceReporter::onReadyRead);
connect(m_socket, &QTcpSocket::disconnected, this, [this]() {
qWarning() << "Disconnected, will reconnect...";
m_heartbeatTimer.stop();
m_reconnectTimer.start();
});
connect(m_socket, &QTcpSocket::errorOccurred, [this](QAbstractSocket::SocketError) {
qWarning() << "Socket error:" << m_socket->errorString();
});
}
void onReadyRead() {
m_recvBuffer.append(m_socket->readAll());
while (parseResponse()) {
// 继续解析
}
}
bool parseResponse() {
if (m_recvBuffer.size() < 8) return false;
QDataStream stream(&m_recvBuffer, QIODevice::ReadOnly);
stream.setByteOrder(QDataStream::BigEndian);
quint32 code, len;
stream >> code >> len;
if (m_recvBuffer.size() < 8 + len) return false;
QByteArray payload = m_recvBuffer.mid(8, len);
handleServerResponse(code, payload);
m_recvBuffer.remove(0, 8 + len);
return true;
}
void handleServerResponse(quint32 code, const QByteArray& payload) {
if (code == 0x200) {
qDebug() << "Server ACK:" << payload;
} else if (code == 0x500) {
qWarning() << "Server ERROR:" << payload;
}
}
void sendHeartbeat() {
QJsonObject hb;
hb["type"] = "heartbeat";
hb["timestamp"] = QDateTime::currentDateTime().toMSecsSinceEpoch();
sendData(hb);
}
void flushPendingData() {
for (const auto& data : m_pendingData) {
sendData(data);
}
m_pendingData.clear();
}
QString m_serverHost;
quint16 m_serverPort;
QTcpSocket* m_socket = nullptr;
QByteArray m_recvBuffer;
QList<QJsonObject> m_pendingData;
QTimer m_reconnectTimer;
QTimer m_heartbeatTimer;
};
# 5.9 速查表
| 类 | 用途 | 关键方法 |
|---|---|---|
QTcpSocket | TCP 客户端 | connectToHost(), readyRead() |
QTcpServer | TCP 服务端 | listen(), nextPendingConnection() |
QUdpSocket | UDP 通信 | bind(), readDatagram(), writeDatagram() |
QNetworkAccessManager | HTTP 客户端 | get(), post(), put() |
QNetworkReply | HTTP 响应 | readAll(), error(), finished() |
QWebSocket | WebSocket 客户端 | open(), sendTextMessage(), ping() |
QDataStream | 二进制序列化 | setVersion(), startTransaction() |
QJsonDocument | JSON 处理 | toJson(), fromJson(), object() |
QCborValue | CBOR 处理 | toCbor(), fromCbor() |
QUrl/QUrlQuery | URL 解析 | scheme(), host(), addQueryItem() |
QHostInfo | DNS 查询 | fromName(), localHostName() |
QNetworkInterface | 网络接口 | allInterfaces(), addressEntries() |
网络编程铁律:
- TCP 必须设计帧协议——魔数 + 长度 + payload,无视粘包必出 bug
QNetworkReply*必须deleteLater(),不能直接 delete- 嵌入式写网络代码必须处理断线重连 + 心跳保活
- 数据上报选 CBOR(紧凑)或 QDataStream(最紧凑),Web API 用 JSON
QDataStream固定版本号,跨设备部署时避免反序列化失败- QUdpSocket 不保证有序和到达——高频传感器场景需自建丢包检测