首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 解析二進制資料流:unpack/pack 深度解析與 C 結構體讀取實戰

     編輯:PHP 綜合知識

前言

PHP 雖以 C 語言開發,但未原生支援 C 結構體(`struct`)。透過 `unpack()`/`pack()` 可實現二進位資料與 PHP 陣列/字串的雙向轉換。本文深度解析格式字元對照表、位序/位寬/對齊陷阱、字串終止符處理,並提供完整讀取 C 結構體二進位檔案的實戰範例。

一、核心函式簽名

string pack(string $format, mixed ...$values): string
array unpack(string $format, string $data): array

二、格式字元完整對照表

字元說明位寬位序/備註
aNUL 填充字串-不足補 `\0`,解包保留尾部 `\0`
ASPACE 填充字串-不足補空格,解包移除尾部空格
h / H十六進位字串-h: 低半位組優先 (LSB),H: 高半位組優先 (MSB)
c / C有號/無號 char1 byte-128~127 / 0~255
s / S有號/無號 short2 bytes機器位序 (Little/Big Endian)
n無號 short (Big Endian)2 bytes網路位序,固定 Big Endian
v無號 short (Little Endian)2 bytes固定 Little Endian
i / I有號/無號 int機器相關通常 4 bytes,視平台而定
l / L有號/無號 long4 bytes機器位序
N無號 long (Big Endian)4 bytes網路位序
V無號 long (Little Endian)4 bytes固定 Little Endian
f / dfloat / double4/8 bytesIEEE 754,機器位序
x忽略 1 byte (NUL)1 byte跳過位移,解包不產生鍵值
X倒退 1 byte1 byte指標後移
@絕對位移填充 NUL-移動到指定絕對位移

三、關鍵陷阱與對策

1. 位序(Endianness)選擇

  • 跨平台/網路傳輸:強制使用 `n`/`N` (Big Endian) 或 `v`/`V` (Little Endian)。
  • 本機 C 程式產生的檔案:通常為 Little Endian (x86),用 `v`/`V`。

2. 結構體對齊(Padding)

C 編譯器為效能在成員間插入填充位元組。PHP `unpack` 不自動處理,需手動插入 `x` 跳過。

// C: struct { char c; int i; } // 可能佔 8 bytes (1+3 padding+4)
// PHP 格式:
$format = 'c c3pad i'; // c3pad 跳過 3 bytes 填充
// 或使用 @ 絕對定位
$format = 'c @4 i'; // 位移 4 讀取 int

3. 字串終止符(`\0`)處理

C 字串以 `\0` 結尾,PHP 字串可包含 `\0`。`unpack('a10', ...)` 會保留尾部 `\0`,導致 `"hello\0world\0"`。

解決: 解包後用 `strtok($str, "\0")` 截取第一個 `\0` 前內容。

4. 有號/無號轉換

PHP 整數為有號。`unpack('C')` 返回 0-255,`unpack('c')` 返回 -128~127。大數值(如 `V` 無號長整數)可能超出 PHP 整數範圍(32 位系統),返回浮點數,需用 `sprintf('%u', $val)` 或 GMP/BCMath 處理。

四、實戰範例:讀取 C 結構體二進位檔案

1. C 端定義與寫入(參考)

// struct_def.h
struct UserRecord {
    char username[20];
    char password[32];
    int  user_id;
    unsigned char status;
}; // 共 20+32+4+1 = 57 bytes (無額外 padding)

// write.c
struct UserRecord user = {"alice", "secret123", 1001, 1};
fwrite(&user, sizeof(user), 1, fp);

2. PHP 端讀取

<?php
$filename = 'users.dat';
$recordSize = 20 + 32 + 4 + 1; // 57 bytes

// 格式字串:對應 C 結構體成員順序
$format = 'a20username a32password Vuser_id Cstatus';
// V: Little Endian 無號長整數 (4 bytes)
// C: 無號字元 (1 byte)

$data = file_get_contents($filename);
$fileSize = strlen($data);
$recordCount = $fileSize / $recordSize;

if ($fileSize % $recordSize !== 0) {
    die("檔案大小非記錄大小整數倍,可能損壞");
}

$users = [];
for ($offset = 0; $offset < $fileSize; $offset += $recordSize) {
    // 解包單筆記錄
    $record = unpack($format, substr($data, $offset, $recordSize));

    // 處理字串終止符
    foreach ($record as $key => &$val) {
        if (is_string($val)) {
            $val = strtok($val, "\0");
        }
    }
    unset($val); // 解除引用

    $users[] = $record;
}

// 輸出
print_r($users);
/*
Array
(
    [0] => Array
        (
            [username] => alice
            [password] => secret123
            [user_id] => 1001
            [status] => 1
        )
)
*/

五、寫入二進位檔案

$user = [
    'username' => 'bob',
    'password' => 'newpass456',
    'user_id'  => 1002,
    'status'   => 1,
];

// pack 格式需與 unpack 一致
$binary = pack(
    'a20 a32 V C',
    $user['username'],
    $user['password'],
    $user['user_id'],
    $user['status']
);

file_put_contents('users.dat', $binary, FILE_APPEND | LOCK_EX);

六、進階:複雜結構體(巢狀、陣列、聯合)

  • 巢狀結構: 遞迴解包,計算子結構大小,分段 `substr` + `unpack`。
  • 固定陣列: 格式字串重複,如 `a10name a10name a10name` 或迴圈處理。
  • 聯合: 同一位移解包多種格式,取適當欄位。
  • 動態長度: 先讀取長度欄位,再根據長度讀取可變資料。

七、效能與替代方案

方案優點缺點
`unpack`/`pack`內建、無依賴、靈活格式字串易錯、大量資料較慢
FFI (PHP 7.4+)直接映射 C 類型、效能高需 PHP 7.4+、學習曲線、安全限制
Protobuf/MessagePack跨語言、Schema 驅動、壓縮率高需預定義 .proto、額外依賴
C 擴展極致效能、原生存取開發維護成本高

八、總結

`unpack`/`pack` 是 PHP 處理二進位資料的核心工具。掌握格式字元語義、位序指定、結構體對齊、字串終止符處理四大關鍵點,即可可靠解析 C 結構體檔案、網路封包、二進位協定。實務上建議封裝為 `BinaryParser` 類別,管理格式定義、位移計算、錯誤處理,提高可維護性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved