news 2026/10/3 12:42:33

拒绝粘包!基于 C++ 手把手带你实现一个自定义 LV 协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拒绝粘包!基于 C++ 手把手带你实现一个自定义 LV 协议

目录

为什么需要这个协议?(解决粘包)

拆解“快递面单” (协议结构)

1. Length(总长度)—— 4字节(固定)

2. MType(消息类型)—— 4字节(固定)

3. IDLength(ID长度)—— 4字节(固定)

4. MID(消息ID)—— 长度可变

5. Body(消息体)—— 长度可变

实战演练 —— 接收方如何拆包裹?

代码实战 —— 怎么打包,怎么拆包?

第一步:明确图片里的协议结构

第二步:发送方打包(拼字符串)

第三步:接收方拆包(切字符串)

第四步:解决粘包和半包(核心中的核心)

第五步:完整测试(把前面的 pack 和 unpack 都拼起来)


首先,我们把这个过程想象成“寄快递”。网络通信就像是一条传送带,发送方要把东西传给接收方,但传送带是连续不断的,接收方怎么知道哪一堆东西是一个完整的包裹呢?

这个协议就是为了解决这个问题而设计的“快递面单规范”

为什么需要这个协议?(解决粘包)

假设你在网上买了两件商品,商家用两个纸箱装好发给你。
但因为快递公司传送带的原因,你收到的时候可能是:

  1. 粘包:两个纸箱被胶带死死缠在一起,变成了一个巨大的箱子。

  2. 半包:一个箱子在路上破了,你先收到了半个箱子,第二天才收到另外半个。

计算机里的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) - 面单上其他所有字段的长度

实战演练 —— 接收方如何拆包裹?

假设你是一个接收方,收到了这串数据,你该怎么解析?

  1. 先读前4个字节(Length):你一看,值是100。

    • 你的心里活动:好的,这个包裹总共有100个字节。后面还有96个字节(因为我已经读了4个了)。

  2. 接着读4个字节(MType):你一看,值是1。

    • 你的心里活动:哦,这是一个RPC请求,我得准备去处理业务逻辑。

  3. 再读4个字节(IDLength):你一看,值是8。

    • 你的心里活动:好的,这个包裹的单号有8个字节长。

  4. 接着读8个字节(MID):你读到了具体的单号,比如10086。

    • 你的心里活动:记住这个单号,等下回复的时候要带上。

  5. 最后读剩下的所有字节(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协议

  1. 打包:Length + MType + IDLength + MID + Body,用+拼起来。

  2. 拆包:先读前 4 个字符拿到Length,就知道后面还有多少字符。

  3. 处理粘包/半包:数据先丢进buffer,循环检查buffer够不够一个完整的包(4 + Length),够了就切出来,不够就等

核心公式:

完整包长度 = 4 + Length
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 12:42:29

浏览器页面休眠、页面冻结对 HLS 直播播放的影响

一、浏览器标签切后台休眠带来的直播问题 很多 HLS 直播业务&#xff0c;用户打开直播页面之后&#xff0c;切到别的浏览器标签页、最小化浏览器窗口&#xff0c;一段时间切回来&#xff0c;直播画面卡住不动&#xff0c;不再更新。 很多开发第一反应怀疑 M3U8 流、CDN 问题&…

作者头像 李华
网站建设 2026/10/3 12:41:25

33 嵌入式操作系统 | 模块七验收:端到端一键验收 + 项目答辩

嵌入式操作系统 | 模块七验收&#xff1a;端到端一键验收 项目答辩 本课程开源地址&#xff08;Gitee&#xff09;&#xff1a;https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git 课件、示例代码与验收脚本都在该仓库&#xff0c;可直接 git clone 或下载 ZIP …

作者头像 李华
网站建设 2026/10/3 12:40:09

五大云厂商大模型广场横评:价格双协议与迁移成本全对比2026

大模型API聚合平台是近两年崛起的基础设施新品类&#xff1a;把多家厂商的推理能力统一成标准接口&#xff0c;一个Key加一个接口地址就能在模型间切换&#xff0c;代码只改一行。它的三大价值是接口统一、模型灵活、成本可控。本文横向对比七牛云AI、硅基流动、阿里百炼、百度…

作者头像 李华
网站建设 2026/10/3 12:37:53

关于C语言的基础知识点

前言 C语言是其他语言的发展初始阶段&#xff0c;这里介绍了关于C语言的基础知识点&#xff0c;以便于后面的运用 基础知识点 必记&#xff1a;写代码必须加头文件 #include<stdio.h> 1.主函数 int main( ){ return 0; } 注意&#xff1a;return 0表示程序成功执行完毕 r…

作者头像 李华