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
相关推荐
Wect13 小时前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
NAGNIP1 天前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
Jony_1 天前
高可用移动网络连接
网络协议
端平入洛1 天前
delete又未完全delete
c++
颜酱1 天前
单调栈:从模板到实战
javascript·后端·算法
chilix2 天前
Linux 跨网段路由转发配置
网络协议
CoovallyAIHub2 天前
仿生学突破:SILD模型如何让无人机在电力线迷宫中发现“隐形威胁”
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
从春晚机器人到零样本革命:YOLO26-Pose姿态估计实战指南
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
Le-DETR:省80%预训练数据,这个实时检测Transformer刷新SOTA|Georgia Tech & 北交大
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
强化学习凭什么比监督学习更聪明?RL的“聪明”并非来自算法,而是因为它学会了“挑食”
深度学习·算法·计算机视觉