pack():依格式字串將資料「打包」成二進位字串。
unpack():依格式字串將二進位字串「解包」為陣列。
兩者是 PHP 處理二進制協議、檔案格式、網路封包、硬體介接的基礎工具。
| 代碼 | 說明 | 位元組序 | 大小 | 範例 |
|---|---|---|---|---|
a | NUL 填充字串 | - | 指定長度 | pack('a10', 'hi') → "hi\0\0\0\0\0\0\0\0" |
A | 空白填充字串 | - | 指定長度 | pack('A10', 'hi') → "hi " |
h | 十六進位字串 (低半位元組優先) | - | - | pack('h*', 'abc') |
H | 十六進位字串 (高半位元組優先) | - | - | pack('H*', 'abc') |
c | 有號字元 | - | 1 byte | -128 ~ 127 |
C | 無號字元 | - | 1 byte | 0 ~ 255 |
s | 有號短整數 | 機器序 | 2 bytes | -32768 ~ 32767 |
S | 無號短整數 | 機器序 | 2 bytes | 0 ~ 65535 |
n | 無號短整數 | Big Endian (網路序) | 2 bytes | 跨平台安全 |
v | 無號短整數 | Little Endian | 2 bytes | Intel x86 原生 |
i | 有號整數 | 機器相關 | 機器相關 | 通常 4 bytes |
I | 無號整數 | 機器相關 | 機器相關 | 通常 4 bytes |
l | 有號長整數 | 機器序 | 4 bytes | -2^31 ~ 2^31-1 |
L | 無號長整數 | 機器序 | 4 bytes | 0 ~ 2^32-1 |
N | 無號長整數 | Big Endian | 4 bytes | 網路協議標準 |
V | 無號長整數 | Little Endian | 4 bytes | Windows/Intel 常用 |
f | 單精度浮點數 | 機器相關 | 4 bytes | IEEE 754 |
d | 雙精度浮點數 | 機器相關 | 8 bytes | IEEE 754 |
x | NUL 位元組 (填充) | - | 1 byte | 對齊用 |
X | 回退 1 位元組 | - | - | 罕用 |
@ | 絕對位置填充 NUL | - | - | pack('@4C', 65) → 第 5 位元組為 'A' |
P | 指標結構 (平台相關) | - | - | 極度危險,慎用 |
Z | 以 NUL 結尾字串 | - | - | 同 a 但保證結尾 NUL |
// 重複計數
pack('C4', 1, 2, 3, 4) // 等同 pack('C*', 1, 2, 3, 4)
pack('N*', 1, 2, 3) // * 表示消耗所有剩餘參數
// 組合結構
$binary = pack('NVC*', 0x12345678, 0x9ABC, 'Hello');
// Big Endian uint32 + Little Endian uint16 + 字串
/**
* 寫入結構化二進制日誌
* 格式:[時間戳 uint32 BE][等級 uint8][訊息長度 uint16 BE][訊息內容]
*/
function writeBinaryLog(string $file, string $level, string $message): void {
$levelMap = ['DEBUG' => 1, 'INFO' => 2, 'WARN' => 3, 'ERROR' => 4];
$levelByte = $levelMap[$level] ?? 0;
$msgLen = strlen($message);
if ($msgLen > 65535) throw new LengthException('訊息過長');
$binary = pack('N C n a*', time(), $levelByte, $msgLen, $message);
file_put_contents($file, $binary, FILE_APPEND | LOCK_EX);
}
/**
* 讀取並解析二進制日誌
*/
function readBinaryLog(string $file): Generator {
$handle = fopen($file, 'rb');
if (!$handle) return;
while (!feof($handle)) {
// 先讀固定頭部 7 bytes (4+1+2)
$header = fread($handle, 7);
if (strlen($header) < 7) break; // 不完整記錄
[$timestamp, $levelByte, $msgLen] = unpack('Ntimestamp/Clevel/nlen', $header);
$message = fread($handle, $msgLen);
if (strlen($message) < $msgLen) break;
$levelStr = array_flip(['DEBUG' => 1, 'INFO' => 2, 'WARN' => 3, 'ERROR' => 4])[$levelByte] ?? 'UNKNOWN';
yield [
'timestamp' => $timestamp,
'level' => $levelStr,
'message' => $message,
];
}
fclose($handle);
}
// 使用
writeBinaryLog('/var/log/app.bin', 'INFO', 'User login: john_doe');
foreach (readBinaryLog('/var/log/app.bin') as $entry) {
echo date('Y-m-d H:i:s', $entry['timestamp']) . " [{$entry['level']}] {$entry['message']}\n";
}
/**
* 構造簡易 TCP 封包 (僅示範結構,實際需計算校驗和)
*/
function buildTcpPacket(
int $srcPort, int $dstPort,
int $seqNum, int $ackNum,
int $flags, // FIN=1, SYN=2, RST=4, PSH=8, ACK=16, URG=32
int $windowSize,
string $payload = ''
): string {
// TCP Header 格式 (RFC 793)
// Source Port (2) | Dest Port (2) | Seq Num (4) | Ack Num (4)
// Data Offset (4 bits) | Reserved (6 bits) | Flags (6 bits) | Window (2)
// Checksum (2) | Urgent Pointer (2) | Options (variable) | Payload
$dataOffset = 5; // 5 * 4 = 20 bytes, 無選項
$reserved = 0;
$offsetFlags = ($dataOffset << 12) | ($reserved << 6) | $flags;
// 校驗和暫設 0,實際需計算偽首部 + TCP 首部 + 負載
$checksum = 0;
$urgentPtr = 0;
$header = pack('n n N N n n n n',
$srcPort, $dstPort, $seqNum, $ackNum,
$offsetFlags, $windowSize, $checksum, $urgentPtr
);
return $header . $payload;
}
// 發送原始封包需 root 權限 + socket_create(AF_INET, SOCK_RAW, IPPROTO_TCP)
// 此處僅示範 pack 結構
/**
* 簡易二進制序列化 (固定結構陣列)
* 結構:[筆數 uint32][每筆: ID uint64][分數 float64][時間戳 uint32]
*/
function serializeScores(array $records): string {
$count = count($records);
$binary = pack('N', $count);
foreach ($records as $rec) {
$binary .= pack('J d N', $rec['id'], $rec['score'], $rec['time']);
}
return $binary;
}
function deserializeScores(string $binary): array {
$offset = 0;
$count = unpack('N', substr($binary, $offset, 4))[1];
$offset += 4;
$records = [];
for ($i = 0; $i < $count; $i++) {
$chunk = substr($binary, $offset, 20); // 8+8+4
[$id, $score, $time] = unpack('Jid/dscore/Ntime', $chunk);
$records[] = ['id' => $id, 'score' => $score, 'time' => $time];
$offset += 20;
}
return $records;
}
// 效能測試:10萬筆資料
$records = [];
for ($i = 0; $i < 100000; $i++) {
$records[] = ['id' => $i, 'score' => mt_rand(0, 10000)/100, 'time' => time()];
}
$start = microtime(true);
$bin = serializeScores($records);
$serializeTime = microtime(true) - $start;
$start = microtime(true);
$decoded = deserializeScores($bin);
$deserializeTime = microtime(true) - $start;
echo "二進制序列化: {$serializeTime}s, 反序列化: {$deserializeTime}s, 大小: " . strlen($bin) . " bytes\n";
// 對比 json_encode/decode 通常快 3-5 倍,體積小 50%+
N/n (Big Endian) 或 V/v (Little Endian),禁用 L/S/I/l/i (機器相關)。a/A 需指定固定長度;變長字串用 n/a* (先寫長度再寫內容) 或 Z (NUL 結尾)。f/d 遵循 IEEE 754,跨語言通常相容,但極大/極小值需測試。fread/fwrite 串流,避免 file_get_contents 載入整個檔案。unpack('Nid', $bin) 回傳 ['id' => 123];若未命名 unpack('N', $bin) 回傳數字鍵 [1 => 123]。