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
相关推荐
仙俊红19 小时前
LeetCode487周赛T2,删除子数组后的最终元素
数据结构·算法
-dzk-1 天前
【代码随想录】LC 59.螺旋矩阵 II
c++·线性代数·算法·矩阵·模拟
风筝在晴天搁浅1 天前
hot100 78.子集
java·算法
Jasmine_llq1 天前
《P4587 [FJOI2016] 神秘数》
算法·倍增思想·稀疏表(st 表)·前缀和数组(解决静态区间和查询·st表核心实现高效预处理和查询·预处理优化(提前计算所需信息·快速io提升大规模数据读写效率
超级大只老咪1 天前
快速进制转换
笔记·算法
树℡独1 天前
ns-3仿真之应用层(五)
服务器·网络·tcp/ip·ns3
m0_706653231 天前
C++编译期数组操作
开发语言·c++·算法
故事和你911 天前
sdut-Java面向对象-06 继承和多态、抽象类和接口(函数题:10-18题)
java·开发语言·算法·面向对象·基础语法·继承和多态·抽象类和接口
qq_423233901 天前
C++与Python混合编程实战
开发语言·c++·算法
TracyCoder1231 天前
LeetCode Hot100(19/100)——206. 反转链表
算法·leetcode