TCP消息边界处理的精妙算法

最近阅读Android的源码,发现了一个挺有意思的代码。

使用TCP发送消息的时候,需要定义一条消息的边界,防止接收方"粘包"。

cpp 复制代码
static std::string PackageString(const std::string& str) {
    // 1. 定义基础开销
    // 开销包括:第一个 '\n',尾部的 '\n' 和 '\f',共 3 个字节
    size_t overhead_length = 3; 

    // 2. 计算如果不算长度前缀的位数,大概需要多少位
    // log10(x) 返回 x 的常用对数。例如 log10(9) ≈ 0.95, log10(10) = 1。
    // static_cast<size_t>(log10(N)) + 1 可以计算数字 N 有几位。
    // 这里计算的是:(原始字符串长度 + 3个开销字节) 需要几位数字来表示。
    size_t str_size_digits = 1 + static_cast<size_t>(log10(str.size() + overhead_length));

    // 3. 计算加上长度前缀本身的位数后,总共需要多少位
    // 这里的逻辑是:总长度 = 原始串 + 开销 + "长度的位数"
    size_t total_size_digits =
            1 + static_cast<size_t>(log10(str.size() + overhead_length + str_size_digits));

    // 4. 边界修正 (核心算法)
    // 如果加上 "长度的位数" 后,导致总长度跨越了进位 (例如从 9 变成了 10),
    // 那么表示总长度的数字位数就会增加 1 (从1位变成2位)。
    // 这种情况只会发生一次(因为增加一位带来的增长很小,不可能再次触发进位)。
    if (total_size_digits != str_size_digits) {
        overhead_length++;
    }

    // 5. 计算最终的总大小
    // 总大小 = 原始数据 + 开销(3或4) + 长度数字本身的位数
    size_t total_size = str.size() + overhead_length + str_size_digits;

    // 6. 格式化输出
    // %zu 是 size_t 的格式化占位符
    return android::base::StringPrintf("%zu\n%s\n\f", total_size, str.c_str());
}

封装后的协议格式如下:

<总长度>\n<原始数据>\n\f

  • <总长度>: 一个 ASCII 数字字符串,表示整个包(包括长度前缀本身、换行符、原始数据和尾部字符)的字节数。
  • \n: 分隔符。
  • <原始数据>: 输入的字符串内容。
  • \n\f: 尾部标记。\f 是换页符 (Form Feed, ASCII 0x0C),通常用作消息结束的哨兵字符。

测试代码

cpp 复制代码
#include <iostream>
#include <string>
#include <cmath>
#include <vector>
#include <cstdio>
#include <memory>

// 模拟 android::base::StringPrintf
template<typename ... Args>
std::string StringPrintf(const std::string& format, Args ... args) {
    int size_s = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; 
    if (size_s <= 0) { throw std::runtime_error("Error during formatting."); }
    auto size = static_cast<size_t>(size_s);
    std::unique_ptr<char[]> buf(new char[size]);
    std::snprintf(buf.get(), size, format.c_str(), args ...);
    return std::string(buf.get(), buf.get() + size - 1); 
}

// 提取出来的 PackageString 函数
static std::string PackageString(const std::string& str) {
    size_t overhead_length = 3;  // \n \n \f.

    // 必须处理空字符串导致 log10(0) 的情况,虽然源码没处理,但逻辑上 str.size() 最小为0,+overhead=3,不会log10(0)
    // log10(3) = 0.47 -> 0 + 1 = 1.
    
    size_t str_size_digits = 1 + static_cast<size_t>(log10(str.size() + overhead_length));
    size_t total_size_digits =
            1 + static_cast<size_t>(log10(str.size() + overhead_length + str_size_digits));

    if (total_size_digits != str_size_digits) {
        overhead_length++;
    }

    size_t total_size = str.size() + overhead_length + str_size_digits;
    return StringPrintf("%zu\n%s\n\f", total_size, str.c_str());
}

void TestInput(const std::string& input, const std::string& desc) {
    std::string pkg = PackageString(input);
    
    std::cout << "--- " << desc << " ---" << std::endl;
    std::cout << "Input Length: " << input.size() << std::endl;
    
    // 把不可见字符转义
    std::cout << "Output String: \"";
    for (char c : pkg) {
        if (c == '\n') std::cout << "\\n";
        else if (c == '\f') std::cout << "\\f";
        else std::cout << c;
    }
    std::cout << "\"" << std::endl;
    
    // 验证长度一致性
    // 解析头部长度
    size_t first_newline = pkg.find('\n');
    std::string len_str = pkg.substr(0, first_newline);
    size_t declared_len = std::stoi(len_str);
    
    std::cout << "Actual Size: " << pkg.size() << std::endl;
    std::cout << "Header Says: " << declared_len << std::endl;
    
    if (pkg.size() == declared_len) {
        std::cout << "VERIFIED" << std::endl;
    } else {
        std::cout << "MISMATCH" << std::endl;
    }
    std::cout << std::endl;
}

int main() {
    // 1. 空字符串测试
    TestInput("", "Empty String");

    // 2. 普通短字符串
    TestInput("hello", "Short String");

    // 3. 临界值测试:长度为6 (总长度从个位数跨越到十位数)
    // 6 + 3(overhead) + 1(digit) = 10 -> 需要修正
    TestInput("123456", "Edge Case: 6 chars");

    // 4. 临界值测试:长度为96 (总长度从2位跨越到3位)
    // 96 + 3(overhead) + 2(digits) = 101 -> 3 digits. 
    std::string s96(96, 'x');
    TestInput(s96, "Edge Case: 96 chars");

    return 0;
}

结果输出

bash 复制代码
--- Empty String ---
Input Length: 0
Output String: "4\n\n\f"
Actual Size: 4
Header Says: 4
VERIFIED

--- Short String ---
Input Length: 5
Output String: "9\nhello\n\f"
Actual Size: 9
Header Says: 9
VERIFIED

--- Edge Case: 6 chars ---
Input Length: 6
Output String: "11\n123456\n\f"
Actual Size: 11
Header Says: 11
VERIFIED

--- Edge Case: 96 chars ---
Input Length: 96
Output String: "102\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\
f"
Actual Size: 102
Header Says: 102
VERIFIED


进程已结束,退出代码为 0
相关推荐
知乎的哥廷根数学学派2 小时前
基于高阶统计量引导的小波自适应块阈值地震信号降噪算法(MATLAB)
网络·人工智能·pytorch·深度学习·算法·机器学习·matlab
松涛和鸣2 小时前
51、51单片机
c语言·网络·单片机·嵌入式硬件·tcp/ip·51单片机
chamu992 小时前
C++ 的可调用对象
开发语言·c++
cici158742 小时前
基于光流场的Demons算法MATLAB实现
人工智能·算法·matlab
千里马-horse2 小时前
Drawing a triangle -- setup -- Base code
c++·vulcan
ADI_OP2 小时前
ADAU1452的开发教程4:常规音频算法的开发(3)
算法·音视频·dsp开发·adi dsp中文资料·adi音频dsp·adi dsp开发教程
txinyu的博客2 小时前
unique_ptr shared_ptr weak_ptr的线程安全问题
c++·安全
CHENKONG_CK2 小时前
晨控CK-FR09EIP与汇川H5U系列PLC配置EtherNet/IP通讯连接手册
网络·网络协议·自动化·rfid
持续学习的程序员+12 小时前
部分离线强化学习相关的算法总结(td3+bc/conrft)
算法