目录
第五步:完整测试(把前面的 pack 和 unpack 都拼起来)
首先,我们把这个过程想象成“寄快递”。网络通信就像是一条传送带,发送方要把东西传给接收方,但传送带是连续不断的,接收方怎么知道哪一堆东西是一个完整的包裹呢?
这个协议就是为了解决这个问题而设计的“快递面单规范”
为什么需要这个协议?(解决粘包)
假设你在网上买了两件商品,商家用两个纸箱装好发给你。
但因为快递公司传送带的原因,你收到的时候可能是:
-
粘包:两个纸箱被胶带死死缠在一起,变成了一个巨大的箱子。
-
半包:一个箱子在路上破了,你先收到了半个箱子,第二天才收到另外半个。
计算机里的 TCP协议 就是这条“传送带”。它只负责把数据源源不断地送过去,但不保证边界。
所以,我们需要一个约定:在每个纸箱的最外面,贴一张固定格式的“快递面单”。 只要看到面单,就知道这个箱子有多大,里面装的是什么
拆解“快递面单” (协议结构)

图片中间那个长条图,就是一个完整的“快递面单”。我们从左到右看:
1. Length(总长度)—— 4字节(固定)
-
这是什么:这是面单的第一行,写着“这个包裹总共有多重(多少字节)”。
-
为什么固定4字节:因为4个字节可以表示很大的数字(最大能表示40多亿),足够描述绝大多数包裹的大小了。
-
怎么用:接收方只要先读这4个字节,就知道“哦,原来后面还有100个字节的数据,那我就死等,直到收满100个字节,我才算收完这一个包裹。”
2. MType(消息类型)—— 4字节(固定)
-
这是什么:这是面单的第二行,写着“这个包裹是干什么用的”。
-
为什么需要:因为网络通信很复杂,有的是请求别人办事(RPC调用),有的是订阅新闻(发布/订阅),有的是注册服务。接收方拿到包裹后,得先看类型,才知道要交给哪个部门去处理。
-
例子:比如
MType=1代表RPC请求,MType=2代表RPC响应。
3. IDLength(ID长度)—— 4字节(固定)
-
这是什么:这是面单的第三行,写着“我的快递单号有多长”。
-
为什么需要:因为后面的“快递单号(MID)”长度是不固定的。有的单号长,有的单号短。所以必须先用一个固定的4字节,告诉接收方“单号占了多少个字节”。
4. MID(消息ID)—— 长度可变
-
这是什么:这是“快递单号”。
-
作用:用来唯一标识这一个包裹。比如你发了3个请求给服务器,服务器回了3个响应,你怎么知道哪个响应对应哪个请求?就是靠这个MID来匹配的。
-
长度:由前面的
IDLength决定。比如IDLength=8,那这里就占8个字节。
5. Body(消息体)—— 长度可变
-
这是什么:这是“包裹里真正装的东西”。
-
作用:也就是实际要传输的业务数据。比如你调用一个“计算加法”的函数,Body里就装着
1+1这个参数。 -
长度:它的长度 = 总长度(Length) - 面单上其他所有字段的长度
实战演练 —— 接收方如何拆包裹?
假设你是一个接收方,收到了这串数据,你该怎么解析?
-
先读前4个字节(Length):你一看,值是
100。-
你的心里活动:好的,这个包裹总共有100个字节。后面还有96个字节(因为我已经读了4个了)。
-
-
接着读4个字节(MType):你一看,值是
1。-
你的心里活动:哦,这是一个RPC请求,我得准备去处理业务逻辑。
-
-
再读4个字节(IDLength):你一看,值是
8。-
你的心里活动:好的,这个包裹的单号有8个字节长。
-
-
接着读8个字节(MID):你读到了具体的单号,比如
10086。-
你的心里活动:记住这个单号,等下回复的时候要带上。
-
-
最后读剩下的所有字节(Body):算一下,总共100字节,我已经用了 4+4+4+8=20 字节,还剩
80字节。你就把这80个字节全读出来。-
你的心里活动:这就是真正的数据内容了。
-
完美! 你成功地从连续不断的网络数据流中,精准地切分出了完整的一条消息
代码实战 —— 怎么打包,怎么拆包?
第一步:明确图片里的协议结构
根据图片,我们的数据包长这样:
[Length 4字符][MType 4字符][IDLength 4字符][MID 变长][Body 变长]
-
Length:后面所有东西的总长度(固定 4 字符)
-
MType:消息类型(固定 4 字符)
-
IDLength:MID 的长度(固定 4 字符)
-
MID:消息 ID(长度由 IDLength 决定)
-
Body:正文(剩下的都是它)
第二步:发送方打包(拼字符串)
#include <iostream>
#include <string>
using namespace std;
// 辅助函数:把数字变成 4 位字符串。比如 5 变成 "0005"
string to4(int num) {
string s = to_string(num);
while (s.length() < 4) s = "0" + s;
return s;
}
// 打包函数
string pack(int mtype, string mid, string body) {
// 1. 算 IDLength(MID 的长度)
int idLen = mid.length();
// 2. 算 Length(后面所有东西的总长度)
// 后面有:MType(4) + IDLength(4) + MID(idLen) + Body(body.length())
int length = 4 + 4 + idLen + body.length();
// 3. 按顺序拼接(这就是打包!)
string packet = to4(length) + to4(mtype) + to4(idLen) + mid + body;
return packet;
}
int main() {
// 发送一条消息:类型1,ID是"1001",正文是"Hello"
string packet = pack(1, "1001", "Hello");
cout << "打包结果: " << packet << endl;
return 0;
}
运行结果:
我们来拆解一下这个结果:
-
0017→ Length(后面总共 17 个字符) -
0001→ MType(类型 1) -
0004→ IDLength(MID 有 4 个字符) -
1001→ MID -
Hello→ Body
第三步:接收方拆包(切字符串)
/*
模拟Lv协议
打包,拆包
*/
/*
数据包长这样子
[Length 4字符][MType 4字符][IDLength 4字符][MID 变长][Body 变长]
*/
#include <iostream>
#include <string>
using namespace std;
// 辅助函数,把数字转化为4为字符串,4转为字符串0004
string to4(int num){
// 转换字符串
string s = to_string(num);
while(s.length() < 4){
// 左边补0
s = "0" + s;
}
return s;
}
// 打包函数
string pack(int mtype, string mid, string body){
// 1、算IDLength长度(MID 的长度)
int idLen = mid.size();
// 2、算length(后面所有东西的总长度)
int length = 4 + 4 + idLen + body.size();
// 3、按顺序拼接(这就是打包)
string packet = to4(length) + to4(mtype) + to4(idLen) + mid + body;
return packet;
}
void unpack(string packet){
// 1、切除前4个字符,这是Length
string lenStr = packet.substr(0,4);
int length = stoi(lenStr);
// 2、切除前4个字符,这是MType
string mtypeStr = packet.substr(4,4);
int mtype = stoi(mtypeStr);
// 3、切除前4个字符,这是LDLength
string idLenStr = packet.substr(8,4);
int idLen = stoi(idLenStr);
// 4、从12个字符开始,切idLen个字符,这是MID
string mid = packet.substr(12,idLen);
// 5. 剩下的全是 Body
string body = packet.substr(12 + idLen);
// 打印结果
cout << "解析结果:" << endl;
cout << " Length = " << length << endl;
cout << " MType = " << mtype << endl;
cout << " IDLength = " << idLen << endl;
cout << " MID = " << mid << endl;
cout << " Body = " << body << endl;
}
int main()
{
// 1、发送方打包
string packet = pack(1, "1001", "hello");
cout << "打包结果:" << packet << endl;
// 2、解包
unpack(packet);
return 0;
}
运行结果:
第四步:解决粘包和半包(核心中的核心)
现实中,网络不会乖乖地一条一条发。我们用一个大口袋(buffer)来装数据。
string buffer = ""; // 大口袋
// 处理口袋里的数据
void processBuffer() {
while (true) {
// 1. 口袋里连 4 个字符(Length)都不够,等下次吧
if (buffer.length() < 4) break;
// 2. 偷看前 4 个字符,算出整个包应该有多长
int length = stoi(buffer.substr(0, 4));
// 3. 整个包的实际总长度 = 4(Length自己) + length(后面的东西)
int totalLen = 4 + length;
// 4. 口袋里够不够一个完整的包?
if (buffer.length() >= totalLen) {
// 够了!切出完整的包
string packet = buffer.substr(0, totalLen);
// 从口袋里删掉已经处理的部分
buffer.erase(0, totalLen);
// 解析这个包(用上面的 unpack 逻辑)
cout << "✅ 解析出一条完整消息: " << packet << endl;
} else {
// 不够!半包,等下次数据来
cout << "⏳ 数据不完整,等待更多数据..." << endl;
break;
}
}
}
// 模拟网络数据到达
void onDataReceived(string data) {
cout << "📩 收到数据: " << data << endl;
buffer += data; // 丢进口袋
processBuffer(); // 尝试处理
}
第五步:完整测试(把前面的 pack 和 unpack 都拼起来)
/*
模拟Lv协议
打包,拆包
*/
/*
数据包长这样子
[Length 4字符][MType 4字符][IDLength 4字符][MID 变长][Body 变长]
*/
#include <iostream>
#include <string>
using namespace std;
// 辅助函数,把数字转化为4为字符串,4转为字符串0004
string to4(int num){
// 转换字符串
string s = to_string(num);
while(s.length() < 4){
// 左边补0
s = "0" + s;
}
return s;
}
// 打包函数
string pack(int mtype, string mid, string body){
// 1、算IDLength长度(MID 的长度)
int idLen = mid.size();
// 2、算length(后面所有东西的总长度)
int length = 4 + 4 + idLen + body.size();
// 3、按顺序拼接(这就是打包)
string packet = to4(length) + to4(mtype) + to4(idLen) + mid + body;
return packet;
}
void unpack(string packet){
// 1、切除前4个字符,这是Length
string lenStr = packet.substr(0,4);
int length = stoi(lenStr);
// 2、切除前4个字符,这是MType
string mtypeStr = packet.substr(4,4);
int mtype = stoi(mtypeStr);
// 3、切除前4个字符,这是LDLength
string idLenStr = packet.substr(8,4);
int idLen = stoi(idLenStr);
// 4、从12个字符开始,切idLen个字符,这是MID
string mid = packet.substr(12,idLen);
// 5. 剩下的全是 Body
string body = packet.substr(12 + idLen);
// 打印结果
cout << "解析结果:" << endl;
cout << " Length = " << length << endl;
cout << " MType = " << mtype << endl;
cout << " IDLength = " << idLen << endl;
cout << " MID = " << mid << endl;
cout << " Body = " << body << endl;
}
// 解决粘包和半包
string buffer = "";
// 处理口袋里的数据
void processBuffer(){
while(true){
// 1、口袋里4个字段都不够,等下次
if(buffer.length() < 4) break;
// 2、偷看一下4个字符,算出这个包有多长
int length = stoi(buffer.substr(0,4));
// 3、整个包的实际总长度
int totalLen = 4 + length;
// 4、口袋能不能装一个完整的包?
if(buffer.length() >= totalLen){
// 够了,切出完整的包
string packet = buffer.substr(0,totalLen);
// 口袋里删掉已经处理的部分
buffer.erase(0, totalLen);
// 解析这个包
cout << "解析出一条完整消息:" << endl;
unpack(packet);
}else
{
// 解决半包
// 不够,半包,等下次数据来
cout << "数据不完整,等待更多数据" << endl;
break;
}
}
}
// 模拟网络数据到达
void on(string data){
cout << "收到数据:" << data << endl;
buffer += data; // 丢进口袋
processBuffer(); //尝试处理
}
int main()
{
// 粘包和贴包
string packet1 = pack(1, "1001", "hello");
string packet2 = pack(1, "1002", "ok");
cout << "场景一:正常处理" << endl;
on(packet1);
cout << "场景二:粘包(一起发)" << endl;
on(packet1 + packet2);
cout << endl << "场景二:粘包(一起发)" << endl;
on(packet1.substr(0,10));
on(packet1.substr(10));
return 0;
}

总结:三句话再次深刻理解LV协议
-
打包:
Length + MType + IDLength + MID + Body,用+拼起来。 -
拆包:先读前 4 个字符拿到
Length,就知道后面还有多少字符。 -
处理粘包/半包:数据先丢进
buffer,循环检查buffer够不够一个完整的包(4 + Length),够了就切出来,不够就等
核心公式:
完整包长度 = 4 + Length
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Frank_refuel/article/details/166991963





