在物聯網通訊、支付網關對接、舊系統整合等場景,PHP 需直接操作二進制位元組流。pack()、unpack()、ord() 是 PHP 處理二進制資料的三大核心函數,本文透過完整實例,從格式字元對照表到實戰通訊協定封裝/解析,帶你掌握這門「冷門但關鍵」的技能。
string pack ( string $format, mixed ...$values )
常用格式字元(完整表見 PHP 官方文檔):
| 字元 | 說明 | 位元組數 | 位序 |
|---|---|---|---|
a | 以 NULL 填充字串 | 依長度 | - |
A | 以空格填充字串 | 依長度 | - |
c | 有號字元 | 1 | - |
C | 無號字元 | 1 | - |
s | 有號短整數 | 2 | 主機序 |
n | 無號短整數 | 2 | 大端序 |
v | 無號短整數 | 2 | 小端序 |
i | 有號整數 | 4/8 | 主機序 |
I | 無號整數 | 4/8 | 主機序 |
l | 有號長整數 | 4 | 主機序 |
N | 無號長整數 | 4 | 大端序 |
V | 無號長整數 | 4 | 小端序 |
f | 單精度浮點數 | 4 | 主機序 |
d | 雙精度浮點數 | 8 | 主機序 |
array unpack ( string $format, string $data [, int $offset = 0 ] )
格式字元與 pack() 相同,回傳關聯陣列,鍵名可自訂(如 "username/A20")。
int ord ( string $character )
以下程式碼展示相同數值用不同格式打包的位元組差異:
<?php
function getAscill($str) {
foreach (str_split($str) as $v) {
echo $v . "=" . ord($v) . "\n";
}
echo "=============\n\n";
}
// A 字串(空格填充)
$str = pack("A*", "中國");
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// H 十六進位(高位在前)
$str = pack("H*", "fffe");
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// C 無號字元
$str = pack("C*", 55, 56, 57);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// i 有號整數(32/64 位視平台)
$str = pack("i", 100);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// s 短整數(2 位元組)
$str = pack("s", 100);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// l 長整數(4 位元組)
$str = pack("l", 100);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// f 單精度浮點
$str = pack("f", 100);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
// d 雙精度浮點
$str = pack("d", 100);
echo $str . "=" . strlen($str) . " 位元組\n";
getAscill($str);
?>
某舊系統要求 TCP 短連線,負載結構如下:
| 欄位 | 長度 | 型別 | 格式字元 |
|---|---|---|---|
| 用戶名 | 20 位元組 | 字串 | A20 |
| 密碼 | 10 位元組 | 字串 | A10 |
| 年齡 | 1 位元組 | 無號字元 | C |
| 出生年月 | 4 位元組 | 無號長整數(大端序) | N |
| 郵箱 | 50 位元組 | 字串 | A50 |
欄位間以 \0 分隔,總長 89 位元組。
<?php
$code = array(
"username" => array("A20", "張三adfb12"),
"pass" => array("A10", "asdf*#1"),
"age" => array("C", 23),
"birthday" => array("N", 19900101), // 大端序無號長整數
"email" => array("A50", "zhangsan@163.com")
);
function packByArr($arr) {
$out = [];
foreach ($arr as $v) $out[] = pack($v[0], $v[1]);
return $out;
}
$stream = join("\0", packByArr($code));
echo "封包長度: " . strlen($stream) . " 位元組\n";
// file_put_contents("packet.bin", $stream); // 寫檔或 socket 發送
?>
<?php
$code = array(
"username" => "A20",
"pass" => "A10",
"age" => "C",
"birthday" => "N",
"email" => "A50"
);
$stream = file_get_contents("packet.bin");
$fields = explode("\0", $stream);
$result = [];
$i = 0;
foreach ($code as $k => $fmt) {
// unpack 回傳陣列,鍵名為 1,2... 需手動映射
$unpacked = unpack($fmt, $fields[$i]);
$result[$k] = array_shift($unpacked); // 取第一個值
$i++;
}
print_r($result);
/* 輸出範例:
Array
(
[username] => 張三adfb12
[pass] => asdf*#1
[age] => 23
[birthday] => 19900101
[email] => zhangsan@163.com
)
*/
?>
文中提到:INT(10) 僅佔 4 位元組,而 CHAR(10) 佔 10 位元組(不含索引開銷)。正確選擇欄位型別可節省 50%+ 儲存空間,並大幅提升索引效能。例如:
TINYINT UNSIGNED (1 byte)SMALLINT/MEDIUMINT/INT UNSIGNEDDECIMAL(10,2) 而非 FLOATCHAR;變長文本 → VARCHARn/N (大端) 或 v/V (小端),禁用 s/i/l 等主機序格式。pack("A10", "超過10字元字串") 會截斷,解包時補空格導致資料髒污。發送前必須 mb_strlen() 校驗。pack("N", 4000000000) 會溢位,建議統一 64 位環境或用 bcmath 處理大數。pack("f", 0.1) 非精確值,金融嚴禁用浮點;改用定點整數(分為單位)或 DECIMAL 字串傳輸。pack/unpack/ord 是 PHP 處理二進制協定的「瑞士刀」。雖然現代 API 多採 JSON/Protobuf,但在舊系統整合、硬體通訊、支付 ISO8583、自定義二進制協定等場景仍不可替代。掌握格式字元、位序、長度校驗三大核心,即可游刃有餘地處理任何位元組流挑戰。