☰
PHP unpack() 函数
2026/10/8 23:17:22 网站建设 项目流程

前言


unpack()用来从一段二进制字符串里按指定的字节布局解出各个字段,返回一个关联数组。它和pack()是一对:pack()把 PHP 值打包成二进制串,unpack()把它拆回来。凡是涉及「自己定义二进制协议」「读取带文件头的二进制格式」的场景,都绕不开这一对函数。


三个最常见的误解:



  1. 以为它的格式串和pack()不是一套。两者完全相同,手册在unpack()的参数说明里直接写着「格式码的解释参见pack()」。学会一套就够。

  2. 以为不写字段名时数组下标从 0 开始。恰恰相反,PHP 手册明确写着:不给元素命名时,会使用从 1 开始的数字下标。更麻烦的是,每个未命名的元素都从 1 重新编号,混用多种未命名格式码时,后面的值会覆盖前面的值。

  3. 以为unpack()能做编码转换。它只管按字节布局拆数据,不做 UTF-8 ↔ GBK 之类的转码;中文文本要用mb_convert_encoding()/iconv()处理。


本文按官方手册讲清unpack()的签名、格式串语法(尤其三种字符串格式码的区别)、字节序的取舍,以及越界解包的问题。


一、签名与格式串语法


unpack(string $format, string $string, int $offset = 0): array | false


  • $format:格式串,由「格式码 + 可选重复次数 + 可选字段名」组成,多个字段之间用/分隔。

  • $string:要拆的二进制数据。

  • $offset:从第几个字节开始解,PHP 7.1.0 起才有这个参数。手册另外注明,自 PHP 7.2.0 起float与double类型同时支持大端和小端。


格式串的构成规则:


格式码 [重复次数] [字段名] / 格式码 [重复次数] [字段名] / ...

<?php // 适用于 PHP 8.0+
$data = "\x04\x00\xa0\x00";

// 每个字段都起名:得到一个以名字为键的关联数组
$named = unpack('cchars/nint', $data);
print_r($named);

// 带重复次数:键名后面会自动补上序号
$repeated = unpack('c2chars/nint', $data);
print_r($repeated);

Array
(
[chars] => 4
[int] => 160
)
Array
(
[chars1] => 4
[chars2] => 0
[int] => 40960
)

注意重复次数放在字段名之前:c2chars表示「两个c,名字前缀是chars」,得到chars1、chars2。


二、格式码速查


下面这张表来自pack()的官方格式码表,unpack()完全适用:




格式码含义



aNUL 填充的字符串

A空格填充的字符串

ZNUL 结尾(ASCIIZ)字符串,做 NUL 填充

h/H十六进制串,低半字节在前 / 高半字节在前

c/C有符号字符 / 无符号字符

s/S有符号短整型 / 无符号短整型(16 位,机器字节序)

v/n无符号短整型(16 位,小端 / 大端字节序)

i/I有符号整型 / 无符号整型(长度与字节序都依赖机器)

l/L有符号长整型 / 无符号长整型(32 位,机器字节序)

V/N无符号长整型(32 位,小端 / 大端字节序)

q/Q有符号 / 无符号长长整型(64 位,机器字节序)

P/J无符号长长整型(64 位,小端 / 大端字节序)

f单精度浮点(长度与表示都依赖机器)

g/G单精度浮点(小端 / 大端字节序)

d双精度浮点(依赖机器)

e/E双精度浮点(小端 / 大端字节序)

x一个 NUL 字节(跳过)

X回退一个字节

@用 NUL 填充到绝对位置



三种字符串格式码的区别


这是最容易记混的一组,手册特别说明了它们对尾部字节的处理:




格式码尾部处理



a保留尾随的 NUL 字节

A去掉所有尾随的 ASCII 空白(空格、制表符、换行、回车、NUL)

Z去掉尾随的 NUL 字节(专门为 NUL 结尾的 C 字符串设计)



所以:读定长文本字段用A(自动去填充),读 C 风格字符串用Z,需要拿到原始字节(包括填充)才用a。


三、字节序:为什么协议里只用固定端序的码


unpack('s', ...)、unpack('l', ...)、unpack('f', ...)、unpack('d', ...)、unpack('i', ...)这些码是机器字节序的——同一份数据在不同 CPU 架构上会解出不同的值。做跨机通信的协议时,这几乎一定会出事。


固定字节序的码才有互操作性:




需要的数据大端(网络字节序)小端



16 位无符号nv

32 位无符号NV

64 位无符号JP

单精度浮点Gg

双精度浮点Ee



网络协议(TCP 报文头等)一律用大端,所以解析时优先选n/N/J/E/G。


四、实战示例


1. 解析 PNG 的文件签名


PNG 文件固定以 8 个字节开头,十进制是137 80 78 71 13 10 26 10:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

$path = __DIR__ . '/image.png';
$fp = fopen($path, 'rb');
if ($fp === false) {
exit("无法打开文件\n");
}

$header = fread($fp, 8);
fclose($fp);

if (strlen($header) !== 8) {
exit("文件太短,不是有效图片\n");
}

$bytes = unpack('C8', $header);
// 不带字段名时键从 1 开始
var_dump($bytes);

$isPng = $bytes === [1 => 137, 2 => 80, 3 => 78, 4 => 71, 5 => 13, 6 => 10, 7 => 26, 8 => 10];
echo $isPng ? "这是 PNG\n" : "不是 PNG\n";

2. 解析自定义二进制包头


假设协议规定:2 字节魔数、1 字节版本、2 字节大端长度、随后是载荷:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

function parsePacket(string $raw): array
{
$headLen = 5; // 2 + 1 + 2
if (strlen($raw) < $headLen) {
throw new RuntimeException('数据不足一个包头');
}

// 每个字段都命名,键名清晰且不会互相覆盖
$head = unpack('a2magic/Cversion/nlength', $raw);
if ($head === false) {
throw new RuntimeException('包头解析失败');
}

// a2 读的是原始字节,逐字节比较更稳妥
if ($head['magic'] !== 'PK') {
throw new RuntimeException('魔数不匹配');
}

$payload = substr($raw, $headLen, $head['length']);
if (strlen($payload) !== $head['length']) {
throw new RuntimeException('载荷长度不足');
}

return [
'magic' => $head['magic'],
'version' => $head['version'],
'length' => $head['length'],
'payload' => $payload,
];
}

// 构造一段测试数据:'PK' + 版本 1 + 长度 3(大端 0x0003)+ 'abc'
$raw = 'PK' . "\x01" . "\x00\x03" . 'abc';
var_dump(parsePacket($raw));

3. 用$offset跳过文件头解析浮点数组


<?php // 适用于 PHP 8.0+,$offset 参数需要 PHP 7.1+
declare(strict_types=1);

// 8 字节头 + 3 个单精度浮点(小端)
$binary = str_repeat("\x00", 8)
. pack('g3', 1.5, -2.25, 3.75);

// PHP 7.1 起可以直接用第三个参数,不用先 substr
$values = unpack('g3v', $binary, 8);
var_dump($values);
// array(3) { ["v1"]=> float(1.5) ["v2"]=> float(-2.25) ["v3"]=> float(3.75) }

在 PHP 7.1 之前的版本上,等价写法是unpack('g3v', substr($binary, 8))。


常见坑点



  1. ❌ 不给字段命名,以为数组下标从 0 开始 —— 手册写明索引从 1 开始。✅ 每个字段都起名字:unpack('C4flags', $data)得到flags1到flags4。



  1. ❌ 写成unpack('c2/n', $data)这种混用未命名字段的形式 —— 每个未命名元素都从 1 重新编号,后面的值会覆盖前面的。手册给出的例子中,unpack("c2/n", "\x32\x42\x00\xa0")的结果只剩两个元素,第一个c的值被n的值覆盖掉了。✅ 每个字段都命名,例如unpack('c2lo/nhi', $data)。



  1. ❌ 用s、l、i、f、d这些机器字节序的码去解析网络数据 —— 同一段字节在大端机和小端机上解出不同的值。✅ 协议字段一律用固定字节序的码:n/N/J/G/E(大端)或v/V/P/g/e(小端)。



  1. ❌ 把解出来的「无符号数」一律当正数用 —— 手册用 Caution 提示,PHP 内部整型是有符号的,解出一个与 PHP 整型同宽的无符号值时,高位为 1 就会得到负数。✅ 32 位值用N/V在 64 位 PHP 上可以安全表示;64 位无符号数超出PHP_INT_MAX时改走 GMP(gmp_import(),需要 GMP 扩展)。



  1. ❌ 用unpack()解文本编码 —— 它按字节布局工作,不做任何字符集转换,直接拿去显示中文会是乱码。✅ 用mb_convert_encoding()/iconv()转码;固定长度的文本字段可以先用A或Z取出再转。



  1. ❌ 不校验长度就解包 —— 数据不足时结果不可靠(返回false或伴随告警,具体诊断在不同版本上不完全一致),解析器会静默产出错误数据。✅ 先算清「这个格式串需要多少字节」,用strlen()校验;用$offset时也要保证$offset + 需要字节数 <= strlen($string)。



  1. ❌ 在需要兼容 PHP 7.1 以下的项目里直接用第三个参数 ——$offset参数是PHP 7.1.0才加入的。✅ 老版本改用substr($string, $offset)再解包。



  1. ❌ 把a、A、Z三者的尾部处理搞混 ——a保留尾随 NUL,A去掉尾随空白(空格、制表、换行、回车、NUL),Z去掉尾随 NUL。✅ 按字段语义选:定长文本用A,NUL 结尾的 C 字符串用Z,需要原始字节才用a。


总结




要点结论



签名`unpack(string $format, string $string, int $offset = 0): array \

$offsetPHP 7.1.0 起提供;老版本用substr()代替

格式串与pack()完全一致:格式码[重复次数][字段名],多字段用/分隔

键名未命名时索引从 1 开始,且多组未命名会互相覆盖,务必逐个命名

字节序s/l/i/f/d依赖机器;协议数据只用n/N/J/E/G(大端)或v/V/P/e/g(小端)

字符串码a保留尾部 NUL,A去尾部空白,Z去尾部 NUL

有符号性PHP 整型有符号,解出同宽无符号值时可能为负;64 位大数走 GMP

编码不做转码,中文要额外用mb_convert_encoding()处理



一句话总结:unpack()的全部风险集中在「字节布局」这四个字上——格式码要和数据源的布局逐字节对齐,字段一律命名,协议数据只用固定字节序的码,解包前先校验长度,做到这几点,它就是一个可靠而精确的二进制解析工具。





需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询