函数讲解)
前言crc32()计算一段字符串的 CRC-32 校验值cyclic redundancy check循环冗余校验返回一个 32 位整数。它最常见的用途是「快速判断两段数据是不是同一份」缓存键的后缀、ETag、内容去重、短链接的 ID 前缀以及检测传输或存储过程中有没有发生意外的字节损坏。这里必须先纠正一个非常常见的误解CRC-32 是校验和不是哈希更不是加密。它的设计目标是「发现随机错误」而不是「抵抗蓄意篡改」。任何人都可以针对一个给定的 CRC-32 值构造出大量碰撞数据因为 CRC 是线性的。所以它能告诉你「文件传丢了几个字节」但绝不能告诉你「这个数据没有被改过」——后者要用hash(sha256, ...)或hash_hmac()。另一个高频困惑是返回值有时是负数。这不是 bug而是 32 位平台上整数符号性导致的本文会给出统一的无符号表示办法。一、签名与返回值crc32(string $string): int它接受字符串返回整数。需要明确的几点CRC-32 是确定性的相同输入永远得到相同输出没有任何随机成分或 salt。这既是它有用的原因也是它不能用于安全场景的原因。它使用的多项式与 gzip、PNG 等格式里的 CRC-32 一致在hash()里对应的算法名是crc32b。输入为空字符串时结果是0。在 64 位 PHP 上int是 64 位有符号整数而 CRC-32 的值域是 0 到 4294967295完全放得下所以结果永远是非负的。但在32 位 PHP上很多老旧的 x86 服务器、部分嵌入式环境int是 32 位有符号的超过 2147483647 的校验值会显示为负数。这会导致「同一份代码在两台机器上算出不同的数」这种极其难查的问题因为负数一旦被写进数据库或拼进缓存键两张表就对不上了。?php // 适用于 PHP 7.0$s hello world;$c crc32($s);printf(默认输出 : %d\n, $c);printf(无符号十进制 : %u\n, $c); // %u 按无符号解释32/64 位都稳printf(十六进制 : %08x\n, $c); // 固定 8 位小写十六进制%u和%08x是跨平台统一表示的稳妥做法%u把整数按无符号解释%08x在 32 位和 64 位平台上都会给出 8 位十六进制因为高 32 位在两种平台上都是 0 或被符号扩展后取低 32 位。二、与 hash() 的 crc32b 等价PHP 的hash()扩展里有一个算法名crc32b它和crc32()是同一种算法的两种出口一个给整数一个给十六进制。?php // 适用于 PHP 7.0$s hello world;$intValue crc32($s);$hexValue hash(crc32b, $s);var_dump($hexValue sprintf(%08x, $intValue)); // bool(true)这个等价关系有两个实际价值。第一需要把校验值作为字符串参与比较或存储时用十六进制更不容易被平台符号性影响。第二hash_file()支持crc32b这个算法名可以在不把整个文件读进内存的情况下算文件的 CRC?php // 适用于 PHP 7.0// 相对大文件边读边算PHP 内部按块处理$path __DIR__ . /data.bin;if (is_readable($path)) {$fileCrc hash_file(crc32b, $path); // 十六进制字符串printf(%s - %s\n, basename($path), $fileCrc);} else {echo 文件不存在或不可读\n;}注意文件路径要用__DIR__拼接绝对路径而不是相对路径——相对路径取决于「当前工作目录」在 CLI 和 Web 两种运行方式下完全可能不同。三、典型用法与它适合的边界适合?php // 适用于 PHP 8.0// 1) 给缓存键加一个短后缀降低不同内容撞到同一个键的概率$key article: . $id . : . sprintf(%08x, crc32($body));// 2) 内容去重先比长度和 CRC再比完整内容CRC 只用来快速淘汰function sameContent(string $a, string $b): bool{if (strlen($a) ! strlen($b)) {return false;}if (crc32($a) ! crc32($b)) {return false; // 绝大多数不同的内容在这里就被排除了}return hash_equals(hash(sha256, $a), hash(sha256, $b)); // 最终裁决}// 3) 传输完整性发送方带 CRC接收方重算$payload some data;$check sprintf(%08x, crc32($payload));printf(payload%s crc%s\n, $payload, $check);第 2 点是 CRC 正确的用法把 CRC 当作廉价的预筛选最终的判定仍然交给密码学哈希。只比 CRC 就断言「内容相同」是不严谨的。不适合用户令牌、会话 ID、密码散列——用random_bytes()和password_hash()CRC 是可逆推的。防篡改、数据签名——用hash_hmac()带密钥或sodium_crypto_generichash()。「防重复提交」的幂等键——如果用户能猜到该内容对应的 CRC就能伪造。四、输出编码与比较的正确姿势CRC 是用来「比」的不是用来「看」的所以比较方式比计算方式更重要。?php // 适用于 PHP 8.0$expected c87258b7; // 别处传过来的十六进制校验值$actual hash(crc32b, hello world);// ✅ 转成同一种规范形式再比忽略大小写差异if (hash_equals(strtolower($expected), strtolower($actual))) {echo 校验一致\n;}// ❌ 用 比较字符串与数字会触发 PHP 8 的语义变化var_dump(c87258b7 0); // bool(false)PHP 8 中字符串与数字比较已改规则var_dump((string) crc32(x) crc32(x)); // 类型不同不要这么比PHP 8 修改了字符串与数字的比较规则数字字符串与数字比较时按数值比较非数字字符串与数字比较时数字会被转成字符串再比。这条规则很绕最稳的做法是两边都显式转成同一种类型用hash_equals()或不要依赖隐式转换。常见坑点❌if (crc32($a) crc32($b))就认定两段内容完全一样。✅ CRC-32 的碰撞是很容易刻意制造的它只适合「快速排除不同」。最终判定用hash(sha256, ...)配合hash_equals()。❌ 把crc32()的结果当作「找不到」的哨兵值$idx crc32($key); if ($idx 0) { /* 当作没有 */ }。✅ 空字符串的 CRC 就是0合法数据也会算出0。要区分「没有」请单独用isset()/array_key_exists()判断而不是用返回值。❌ 在 32 位机器上直接输出echo crc32(hello world);得到负数写进数据库后再和 64 位机器算的值比较就不相等。✅ 统一用sprintf(%08x, crc32($s))或sprintf(%u, crc32($s))把结果规范化成字符串后再存储和比较。❌ 用crc32()生成短链接 ID 或优惠券码认为 32 位足够「不可猜」。✅ 可枚举、可碰撞。需要不可猜就用bin2hex(random_bytes(16))需要短就用base_convert()或哈希截断并且加上服务端的唯一性校验。❌ 用crc32()校验「登录令牌有没有被改过」即crc32($user . $secret)这种自拼的签名。✅ 这是典型的「自己造 MAC」。用hash_hmac(sha256, $data, $secret)校验时用hash_equals()。❌ 把hash(crc32b, $s)和hash(crc32, $s)当成同一个东西互换使用。✅ 只有crc32b与crc32()的十六进制形式对应hash()里还有其他名字相近的算法别想当然地等价替换。❌ 用crc32(file_get_contents($bigFile))校验大文件。✅ 这会把整个文件读进内存。用hash_file(crc32b, $path)它按块读取。❌crc32($arr)或crc32(null)直接调用。✅ 参数是string类型数组会抛TypeErrornull在 PHP 8 会被转换成空字符串并算出0还会出一条废弃提示显式传更清楚。总结问题结论签名crc32(string $string): int值域0 ~ 429496729532 位无符号32 位平台结果可能显示为负数需用%u/%08x规范化与hash()hash(crc32b, $s)等于sprintf(%08x, crc32($s))文件版本hash_file(crc32b, $path)是否安全否。不是哈希算法对抗攻击用的更不是加密正确用途缓存键后缀、内容去重的预筛选、传输完整性检测把crc32()定位成「廉价的完整性探测工具」就不会用错它能高效地发现意外损坏但对蓄意篡改毫无抵抗力。凡是涉及安全语义的地方——令牌、签名、密码——一律换成random_bytes()、hash_hmac()或password_hash()。