多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

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

拒绝粘包!基于 C++ 手把手带你实现一个自定义 LV 协议 目录为什么需要这个协议解决粘包拆解“快递面单” 协议结构1. Length总长度—— 4字节固定2. MType消息类型—— 4字节固定3. IDLengthID长度—— 4字节固定4. MID消息ID—— 长度可变5. Body消息体—— 长度可变实战演练 —— 接收方如何拆包裹代码实战 —— 怎么打包怎么拆包第一步明确图片里的协议结构第二步发送方打包拼字符串第三步接收方拆包切字符串第四步解决粘包和半包核心中的核心第五步完整测试把前面的 pack 和 unpack 都拼起来首先我们把这个过程想象成“寄快递”。网络通信就像是一条传送带发送方要把东西传给接收方但传送带是连续不断的接收方怎么知道哪一堆东西是一个完整的包裹呢这个协议就是为了解决这个问题而设计的“快递面单规范”为什么需要这个协议解决粘包假设你在网上买了两件商品商家用两个纸箱装好发给你。但因为快递公司传送带的原因你收到的时候可能是粘包两个纸箱被胶带死死缠在一起变成了一个巨大的箱子。半包一个箱子在路上破了你先收到了半个箱子第二天才收到另外半个。计算机里的TCP协议就是这条“传送带”。它只负责把数据源源不断地送过去但不保证边界。所以我们需要一个约定在每个纸箱的最外面贴一张固定格式的“快递面单”。只要看到面单就知道这个箱子有多大里面装的是什么拆解“快递面单” 协议结构图片中间那个长条图就是一个完整的“快递面单”。我们从左到右看1. Length总长度—— 4字节固定这是什么这是面单的第一行写着“这个包裹总共有多重多少字节”。为什么固定4字节因为4个字节可以表示很大的数字最大能表示40多亿足够描述绝大多数包裹的大小了。怎么用接收方只要先读这4个字节就知道“哦原来后面还有100个字节的数据那我就死等直到收满100个字节我才算收完这一个包裹。”2. MType消息类型—— 4字节固定这是什么这是面单的第二行写着“这个包裹是干什么用的”。为什么需要因为网络通信很复杂有的是请求别人办事RPC调用有的是订阅新闻发布/订阅有的是注册服务。接收方拿到包裹后得先看类型才知道要交给哪个部门去处理。例子比如MType1代表RPC请求MType2代表RPC响应。3. IDLengthID长度—— 4字节固定这是什么这是面单的第三行写着“我的快递单号有多长”。为什么需要因为后面的“快递单号MID”长度是不固定的。有的单号长有的单号短。所以必须先用一个固定的4字节告诉接收方“单号占了多少个字节”。4. MID消息ID—— 长度可变这是什么这是“快递单号”。作用用来唯一标识这一个包裹。比如你发了3个请求给服务器服务器回了3个响应你怎么知道哪个响应对应哪个请求就是靠这个MID来匹配的。长度由前面的IDLength决定。比如IDLength8那这里就占8个字节。5. Body消息体—— 长度可变这是什么这是“包裹里真正装的东西”。作用也就是实际要传输的业务数据。比如你调用一个“计算加法”的函数Body里就装着11这个参数。长度它的长度 总长度Length - 面单上其他所有字段的长度实战演练 —— 接收方如何拆包裹假设你是一个接收方收到了这串数据你该怎么解析先读前4个字节Length你一看值是100。你的心里活动好的这个包裹总共有100个字节。后面还有96个字节因为我已经读了4个了。接着读4个字节MType你一看值是1。你的心里活动哦这是一个RPC请求我得准备去处理业务逻辑。再读4个字节IDLength你一看值是8。你的心里活动好的这个包裹的单号有8个字节长。接着读8个字节MID你读到了具体的单号比如10086。你的心里活动记住这个单号等下回复的时候要带上。最后读剩下的所有字节Body算一下总共100字节我已经用了 444820 字节还剩80字节。你就把这80个字节全读出来。你的心里活动这就是真正的数据内容了。完美你成功地从连续不断的网络数据流中精准地切分出了完整的一条消息代码实战 —— 怎么打包怎么拆包第一步明确图片里的协议结构根据图片我们的数据包长这样[Length 4字符][MType 4字符][IDLength 4字符][MID 变长][Body 变长]Length后面所有东西的总长度固定 4 字符MType消息类型固定 4 字符IDLengthMID 的长度固定 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. 算 IDLengthMID 的长度 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() { // 发送一条消息类型1ID是1001正文是Hello string packet pack(1, 1001, Hello); cout 打包结果: packet endl; return 0; }运行结果我们来拆解一下这个结果0017→ Length后面总共 17 个字符0001→ MType类型 10004→ IDLengthMID 有 4 个字符1001→ MIDHello→ 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. 整个包的实际总长度 4Length自己 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
返回列表