云计算百科
云计算领域专业知识百科平台

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

目录

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

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

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

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

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

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

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

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

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

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

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

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

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

第五步:完整测试(把前面的 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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 拒绝粘包!基于 C++ 手把手带你实现一个自定义 LV 协议
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!