必须用SHA-256等确定性哈希算法(如OpenSSL的EVP_DigestInit_ex),禁用std::hash;块大小建议2^n(如64KB);签名文件需固定长度结构+随机读支持;memcmp比std::equal更高效。

怎么用 std::hash 或 openssl/evp.h 生成稳定块哈希
直接调 std::hash 对二进制块做哈希是错的——它不保证跨平台、跨编译器结果一致,也不支持自定义字节范围。生产环境必须用确定性哈希算法,比如 SHA-256。
推荐用 OpenSSL 的 EVP_DigestInit_ex + EVP_DigestUpdate,每次只喂入固定大小的块(如 64KB),避免内存暴涨:
unsigned char hash[SHA256_DIGEST_LENGTH]; EVP_MD_CTX* ctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); EVP_DigestUpdate(ctx, block_data, block_size); // block_data 是 uint8_t* EVP_DigestFinal_ex(ctx, hash, &len); EVP_MD_CTX_free(ctx);
- 别用
std::hash<:string></:string>包装二进制数据——std::string会把第一个\0当结束符,截断内容 - 块大小建议设为 2^n(如 4KB / 64KB),对齐文件系统页和内存拷贝效率
- Windows 上若链接 OpenSSL 静态库,注意
/MD和/MT运行时一致性,否则EVP_MD_CTX_new可能崩溃
如何避免全量重算导致同步卡顿
增量同步的核心不是“比完再传”,而是“边读边判”。一旦发现某块哈希不匹配,立刻标记该块需传输,并跳到下一块——绝不回溯、不缓存整文件。
- 用
mmap(Linux/macOS)或CreateFileMapping(Windows)映射大文件,避免fread多次系统调用开销 - 哈希比对必须按块顺序进行,但传输可以异步:把待传块索引推入队列,由独立线程调
sendfile或WriteFile - 不要在比对循环里做磁盘写入或网络发包——IO 会拖慢整个哈希流水线
- 如果源文件被并发修改,需在开始前用
fstat记录st_mtime和st_size,比对中途发现变化就中止,防止块错位
块签名怎么存才方便快速查找与去重
签名文件不是日志,不能追加写。它得支持随机读、支持快速定位某偏移对应的哈希值,还要能应对文件截断或扩展。
立即学习“C++免费学习笔记(深入)”;
- 结构用固定长度记录:
struct { uint64_t offset; uint8_t hash[32]; },这样offset / block_size就是数组下标 - 签名文件本身用
truncate()配合pwrite()更新,避免覆盖破坏旧数据;新块追加时先lseek到末尾再写 - 客户端同步前先读签名头判断版本(比如前 4 字节存格式版本号),避免用 v2 签名解析 v1 文件导致越界读
- 别把签名存在和源文件同目录下并叫
.sig——某些备份工具或 IDE 会自动忽略或扫描它,引发误删或性能抖动
为什么 memcmp 比 std::equal 更适合哈希比对
哈希值是原始字节数组,不是 C++ 容器。用 std::equal 要传迭代器,而 memcmp 直接比内存块,汇编层常被优化成 SIMD 指令,实测快 2–3 倍。
if (memcmp(local_hash, remote_hash, SHA256_DIGEST_LENGTH) != 0) {
need_transfer.push_back(block_idx);
}
-
memcmp要求两块内存非空且长度一致,所以传参前必须检查SHA256_DIGEST_LENGTH是否等于目标长度,否则未定义行为 - 别用
==比较两个std::array<uint8_t></uint8_t>——默认比较是逐元素,没内联优化时反而更慢 - 某些 sanitizer(如 UBSan)会对跨对象
memcmp报告“invalid memory access”,此时改用std::equal是权衡,但仅限调试阶段
块哈希的稳定性、IO 调度粒度、签名存储结构这三处,任意一个没对齐真实场景,增量同步就会退化成全量拷贝——尤其是当文件有局部小修改却触发大量块哈希重算时,问题最隐蔽。



















