e直在momo想你的Frank头像
关注

拒绝粘包!基于 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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Frank_refuel/article/details/166991963

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--