ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP处理二进制数据怎么避免乱码

PHP处理二进制数据怎么避免乱码 前言二进制数据乱码的表现形式五花八门但症状往往高度一致上传的图片存进数据库再取出来就打不开了用十六进制编辑器一看开头多了三个字节一个本来能正常解析的压缩包经过一次顺手写的转码后gzdecode报错接口返回的 JSON 里带了个二进制字段结果整个响应体变成了空白页。这些故障的根因可以用一句话概括二进制数据被当成了文本处理。PHP 的字符串本质上就是字节数组byte array它本身没有字符集概念这就给了二进制数据一个天然的容器但同时PHP 又提供了一大批会悄悄做编码转换的函数——mb_*系列、htmlspecialchars()、json_encode()、部分字符串函数还有数据库连接的字符集设置。任何一次不必要的转换都会破坏字节的完整性而这种破坏常常是静默的。本文按先分类、再防护的顺序讲搞清三种性质完全不同的乱码、选对二进制安全的编码容器、在数据库与文件读写里保住字节。文中示例需要 PHP 8.0 及以上涉及版本差异的地方会单独标明。一、先把乱码分成三类三类问题的现象相似解法完全不同混在一起查会绕远路类型触发点典型症状是否丢数据编码转换型mb_convert_encoding、htmlspecialchars、连接字符集非法字节被替换、字节数变化会且不可逆函数误用型用mb_*处理二进制、用substr截断多字节文本长度对不上、截断处出现半个字符会展示型输出到浏览器或终端时的字符集不符明明字节完好显示成问号或方块不会第三类最容易让人误判数据本身是好的只是显示环境不对。判断方法很简单——比对字节数。用strlen()和bin2hex()在存之前和取之后各打印一次字节完全一致就说明数据没坏问题在展示层。?php // 判断二进制数据是否在流转中受损比对字节数与十六进制 declare(strict_types1); $raw random_bytes(8); echo len , strlen($raw), PHP_EOL; echo hex , bin2hex($raw), PHP_EOL; echo sha256 , hash(sha256, $raw), PHP_EOL; // 故意做一次看起来无害的 UTF-8 转换哈希与字节数都会变 $mangled mb_convert_encoding($raw, UTF-8, UTF-8); echo mangled len , strlen($mangled), PHP_EOL; echo mangled sha256 , hash(sha256, $mangled), PHP_EOL;二、选对容器hex、base64 与 pack/unpack二进制数据要跨越只认文本的边界URL、JSON、Cookie、日志、纯文本配置就必须换一种表示法。三种常用容器各有定位容器体积膨胀可读性典型用途bin2hex()/hex2bin()2 倍高便于人工比对短标识、调试输出、哈希比对base64_encode()/base64_decode()约 1.33 倍低传输、存文本字段、URL 参数pack()/unpack()无膨胀无定长结构、协议报文、文件头base64_decode()一定要开严格模式否则遇到被污染的内容会静默返回错误的结果?php // 需要 PHP 8.0 及以上 declare(strict_types1); $raw random_bytes(32); $b64 base64_encode($raw); // 用于传输/存储 $hex bin2hex($raw); // 用于日志/比对 echo base64_decode($b64, true) $raw ? base64 往返成功\n : base64 往返失败\n; echo hex2bin($hex) $raw ? hex 往返成功\n : hex 往返失败\n; // URL 安全的变体base64url替换 / 并去掉填充之后记得补回填充再解码 $urlSafe rtrim(strtr($b64, /, -_), ); $padded str_pad($urlSafe, (int) (ceil(strlen($urlSafe) / 4) * 4), , STR_PAD_RIGHT); echo base64_decode($padded, true) $raw ? base64url 往返成功\n : base64url 往返失败\n;pack()与unpack()是处理定长二进制结构的正解。下面这个例子打包了一个 16 字节的报文头格式为4 字节魔数、1 字节版本、2 字节标志大端、1 字节保留位、4 字节负载长度后面跟 4 字节负载?php // pack_demo.php —— 需要 PHP 8.0 及以上 declare(strict_types1); $payload \x00\xff\x10\x80; // 故意包含不可打印字节与 0xFF // a4 4 字节字符串不足补 NUL、C 无符号字符、n 大端 16 位、N 大端 32 位 $packet pack(a4CnCN, HDR1, 2, 1, 0, strlen($payload)) . $payload; echo total , strlen($packet), PHP_EOL; echo hex , bin2hex($packet), PHP_EOL; // 解析每个格式码后跟一个名字用 / 串起来 $head substr($packet, 0, 12); $info unpack(a4magic/Cversion/nflags/Creserved/Nlength, $head); print_r($info); $body substr($packet, 12); echo payload hex , bin2hex($body), PHP_EOL; echo payload len ok , var_export(strlen($body) $info[length], true), PHP_EOL;输出是确定的可以拿来对照total 16 hex 48445231020001000000000400ff1080 Array ( [magic] HDR1 [version] 2 [flags] 1 [reserved] 0 [length] 4 ) payload hex 00ff1080 payload len ok true有几个细节值得记住a与A的区别在于A会去掉尾部的空格和 NUL处理真正的二进制字段要用an/N是大端v/V是小端协议里写错字节序是最常见的低级错误而它造成的现象就是值大得离谱或完全对不上。unpack()返回的是关联数组键名由你指定所以永远不要依赖返回顺序。三、在数据库与文件读写里保住字节数据库这一侧原则是用二进制类型存二进制用文本类型存文本。把二进制塞进TEXT/VARCHAR字段会撞上两件事MySQL 会按该列的字符集校验传入的字节序列非法序列要么报错要么被截断即使侥幸存进去一次ALTER TABLE ... CONVERT TO CHARACTER SET迁移就可能把字节改得面目全非。正确做法是用BLOB/VARBINARY或者干脆存 base64 文本体积涨 1/3但可读、可搜索、跨库迁移安全。?php // 需要 PHP 8.0 及以上pdo_mysql 扩展 declare(strict_types1); $pdo new PDO(mysql:host127.0.0.1;port3306;dbnameapp;charsetutf8mb4, app, secret, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, ]); $pdo-exec(CREATE TABLE IF NOT EXISTS blobs ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, data LONGBLOB NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4); $raw random_bytes(1024); // 写法一小数据直接绑定字符串 $stmt $pdo-prepare(INSERT INTO blobs (name, data) VALUES (?, ?)); $stmt-execute([bin- . bin2hex(random_bytes(4)), $raw]); // 写法二大文件用流绑定避免把整个文件读进内存 $fp fopen(php://temp, rb); fwrite($fp, $raw); rewind($fp); $stmt $pdo-prepare(INSERT INTO blobs (name, data) VALUES (:n, :d)); $stmt-bindValue(:n, stream); $stmt-bindParam(:d, $fp, PDO::PARAM_LOB); $stmt-execute(); // 校验取回来必须与写入时逐字节一致 $got $pdo-query(SELECT data FROM blobs ORDER BY id DESC LIMIT 1)-fetchColumn(); echo strlen($got) strlen($raw) ? 长度一致\n : 长度不一致\n; echo hash(sha256, $got) hash(sha256, $raw) ? 内容一致\n : 内容已被改变\n;查字节长度时用LENGTH()字节而不是CHAR_LENGTH()字符这也是一个一眼区分存的是字节还是文本的小技巧SELECT id, name, LENGTH(data) AS bytes FROM blobs ORDER BY id;文件与 HTTP 输出这一侧重点是别让任何一层替你做转换打开文件用fopen($path, rb)Windows 上漏掉b会让换行字节被改写。大文件用stream_copy_to_stream()直传不要file_get_contents()全读进内存。输出前确认没有任何输出header(Content-Type: application/octet-stream)和Content-Length字节数用strlen()必须发在任何内容之前。源码文件不要带 BOM\xEF\xBB\xBF会在第一个字节就被输出出去紧接着header()就会报headers already sent而这个报错信息和二进制输出看起来毫不相关。?php // 需要 PHP 8.0 及以上以附件形式下发二进制内容 declare(strict_types1); $data random_bytes(256); $hash hash(sha256, $data); if (headers_sent($file, $line)) { // 一旦已经有输出header() 必然失败这里提前发现自己埋的雷 throw new RuntimeException(已有输出无法发送响应头位置: {$file}:{$line}); } header(Content-Type: application/octet-stream); header(Content-Length: . strlen($data)); // 字节数不是字符数 header(X-Content-SHA256: . $hash); echo $data;常见坑点❌ 顺手对二进制数据调用mb_convert_encoding($bin, UTF-8, UTF-8)当作清洗✅ 它是不可逆的非法字节会被替换成替换字符字节数随之变化。要校验请用mb_check_encoding()只做判断不做转换❌ 把二进制塞进TEXT/VARCHAR字段或存进utf8mb4的表指望反正也是字符串✅ 用BLOB/VARBINARY存原始字节或存 base64 文本连接字符集设成utf8mb4是对的但别指望它保护二进制列❌ 对二进制调用htmlspecialchars()做转义✅ PHP 8.0 及以前遇到非法 UTF-8 会直接返回空字符串输出凭空消失PHP 8.1 起默认 flags 带了ENT_SUBSTITUTE会替换成替换字符但字节已经丢了。二进制内容要转义时应先 base64❌ 把二进制直接塞进json_encode()再用json_decode()取回来✅ 非法 UTF-8 会让json_encode()返回falseJSON_ERROR_UTF8整个响应变成空。先base64_encode()或bin2hex()需要排查错误时配合JSON_THROW_ON_ERROR❌ 用mb_strlen()/mb_substr()去量二进制数据的长度和切片✅ 二进制场景一律用strlen()和substr()它们是字节安全的mb_*只处理已知编码的文本❌ 用Content-Length: 字符串长度但把数据当字符数算或者依赖mb_strlen()✅Content-Length是字节数用strlen()下载截断或缺字节多半就是这里算错了❌ 源码文件带 UTF-8 BOM或者在输出二进制之前有空格、空行、?之后的多余字符✅ 源码不带 BOM纯 PHP 文件省略结尾的?输出前用headers_sent()自检❌ 用mb_detect_encoding()去猜二进制数据的编码并据此转换✅ 它只是启发式猜测对二进制毫无意义来源编码应当是已知信息协议约定、数据库列定义不该猜总结场景危险做法安全做法跨文本边界传输直接拼进 JSON/URLbase64_encode()/bin2hex()解析定长结构手写substr偏移 位运算pack()/unpack()声明格式数据库存储TEXT列存原始字节BLOB/VARBINARY或 base64 文本长度计算mb_strlen()strlen()字节转义与序列化htmlspecialchars()/ 裸json_encode()先 base64 再交给文本层文件读写fopen($f, r)、整体读入内存fopen($f, rb)stream_copy_to_stream()输出下载中途 echo、忘算Content-Length先headers_sent()自检用strlen()算长度避免二进制乱码的核心只有一条心法先想清楚这段数据是字节还是文本然后只在文本的世界里做编码转换。二进制数据在 PHP 内部就应该一路保持为原始字符串只在必须跨界的瞬间才换成 base64 或 hex进了数据库就用二进制列。守住这条线绝大多数取出来就不一样了的问题根本不会发生。
返回列表