JSON 与 TCP 帧之间的转换**,本质上就是应用层协议的封装(Encoding)(Decoding)。**
很多机器人 SDK、后端服务器、物联网设备都会做这一层。下面以一个完整流程说明。
一、为什么需要转换?
JSON 只是一个字符串,例如:
{
"seq": 1,
"command": "move",
"speed": 0.5
}
但是 TCP 并不知道哪里是一条完整消息。
TCP 是字节流协议(Stream):
发送:
JSON1
JSON2
JSON3
↓
TCP
↓
接收:
JSON1JSON2JSON3
或者:
JSON1
↓
TCP
↓
第一次收到:
{
"seq"
第二次收到:
":1,"command"
第三次收到:
":"move"...}
也就是说:
TCP 不保证一次 send() 对应一次 recv()。
所以必须自己定义"一帧(Frame)"。
二、什么叫 TCP 帧(Frame)
帧就是:
一条完整消息的数据格式。
例如:
+----------------+
| Frame 1 |
+----------------+
| Frame 2 |
+----------------+
| Frame 3 |
+----------------+
TCP 负责运输。
帧由应用层定义。
三、最常见的两种封包方式
方法一:长度字段(最常见)
很多 SDK 都采用这种方式。
例如:
长度(4字节)
JSON数据
假设 JSON:
{"cmd":"move"}
长度:
14
发送:
+------------+-------------------+
| 00 00 00 0E| {"cmd":"move"} |
+------------+-------------------+
服务器收到:
先读4字节
↓
知道后面还有14字节
↓
继续读取14字节
↓
解析JSON
整个过程如下:
JSON
↓
计算长度
↓
前面加4字节长度
↓
TCP发送
接收:
TCP
↓
读取长度
↓
读取正文
↓
JSON
↓
QJsonDocument::fromJson()
方法二:分隔符
例如:
{"cmd":"move"}\n
发送:
{"cmd":"move"}\n
{"cmd":"stop"}\n
{"cmd":"left"}\n
服务器一直读:
buffer += socket.readAll();
当发现:
\n
说明:
一条消息结束。
于是:
取出
↓
{"cmd":"move"}
↓
解析JSON
四、JSON → TCP 帧(编码)
假设:
cpp
QJsonObject obj;
obj["seq"] = 1;
obj["command"] = "move";
obj["speed"] = 0.5;
第一步:
生成 JSON
cpp
QByteArray json =
QJsonDocument(obj).toJson(QJsonDocument::Compact);
得到:
cpp
{"seq":1,"command":"move","speed":0.5}
第二步:
封装 Frame。
如果使用换行协议:
cpp
json.append('\n');
变成:
{"seq":1,"command":"move","speed":0.5}\n
第三步:
发送
cpp
socket.write(json);
流程:
QJsonObject
↓
QJsonDocument
↓
QByteArray
↓
追加 '\n'
↓
socket.write()
五、TCP 帧 → JSON(解码)
收到:
{"seq":1,"command":"move","speed":0.5}\n
读取:
cpp
buffer += socket.readAll();
寻找:
\n
找到以后:
QString line
↓
{"seq":1,"command":"move","speed":0.5}
然后:
cpp
QJsonDocument doc =
QJsonDocument::fromJson(line.toUtf8());
得到:
cpp
QJsonObject obj = doc.object();
然后:
cpp
obj["command"].toString();
得到:
move
流程:
socket.readAll()
↓
buffer
↓
找到 '\n'
↓
取出一行
↓
QJsonDocument::fromJson()
↓
QJsonObject
↓
command
六、为什么要单独做这一层?
将 JSON 与 TCP 的转换独立出来(例如 Protocol::encode() / Protocol::decode())有几个好处:
职责分离:业务代码只负责构造命令,不需要关心网络传输细节。
易于维护:如果通信协议从"换行分隔"改成"长度前缀",只需要修改协议层,业务层几乎不用改动。
便于测试:协议编码和解码可以独立编写单元测试。
方便扩展:未来可以将 JSON 替换为 Protobuf、MessagePack 等序列化格式,而不影响上层 API。