轉載請註明文章出處:https://tlanyan.me/php-pack-and-unpack-functionsphp
PHP有兩個重要的冷門函數:pack
和unpack
。在網絡編程,讀寫圖像文件等場景,這兩個函數幾乎必不可少。鑑於文件讀寫/網絡編程,或者說字節流處理的重要性,掌握這兩個函數是邁向高級PHP編程的基礎。web
本文先介紹字節
和字符
的區別,說明兩個函數存在的必要性和重要性。而後介紹基本用法和使用場景,讓讀者對其有大致瞭解,爲實際使用中奠基基礎。算法
PHP的優點是簡單易用,熟練運用 字符串 和 數組 相關函數就能抗住通常的需求。平常工做中多用到字符串,因此PHP開發對字符都比較熟悉,稍微資深點基本能也能弄清字符編碼。但字符的伴生概念:字節,很多PHP開發並不知曉/熟悉。數據庫
這不怪他們。PHP世界裏極少出現「字節(流)」的概念:沒有byte
關鍵字(固然也沒有char
),官方文檔也沒提字節;沒有原生的數組支持(經常使用的array
實際上是hashtable
);固然字符串(string)能表達其餘語言中的字節數組(Byte Array, byte[]
)。編程
字節和字符有什麼聯繫和區別呢?簡單來講字節是計算機存儲和操做的最小單位,字符是人們閱讀的最小單位;字節是存儲(物理)概念,字符是邏輯概念;字節表明數據(內涵和本質),字符表明其含義;字符由字節組成。數組
舉幾個例子說明二者區別:「中國」包含2個字符,GBK編碼表示須要4個字節,UTF-8編碼須要6個字節;數字「1234567890」,包含10個字符,用int32
類型表示只需4個字節;下面的圖片佔用42582個字節,用字符表示是「我老婆」,只佔用3個字符:網絡
再舉一個經常使用的例子說明字符和字節的區別。開發中咱們經常使用md5
算法獲取數據的哈希值,算法返回一個128位(bit)的數據(16個字節)。爲方便查看其值,人們約定成俗地用十六進制表示,結果就是咱們熟知的32位長度的字符串(不區分大小寫)。32長度字符串不是md5
算法的必然結果,16字節數據纔是其本質。若是你願意,能夠用一個小於2^128
的數字表示哈希結果,也能夠將16字節base64
編碼後做爲其結果。因此經常使用的32位哈希值與md5
返回的16字節關係爲:一個是字符表示,另外一個則是其本質(字符數組)(PHP的md5
函數第二個參數值爲true
即可獲得16字節數據,或hash
函數第三個參數爲true
)。session
相關概念還有字節序、字符編碼等,本文不作展開。感興趣的讀者可參考本人以前的博客「文件和字符編碼」或相關材料。架構
PHP中專門處理字符串的函數有幾十個,加上正則、時間等函數,字符串處理的函數不下百個。相比之下字節處理門庭冷落,相關函數寥寥無幾。除了經常使用的ord/chr
,哈希加密函數返回的原始字節、openssl庫的openssl_random_pseudo_bytes
等函數真正處理或返回 字節外,最重要的兩個字節處理函數是pack
和unpack
。dom
本節從問題引出pack
函數的使用。
考慮一個簡單的問題:宇宙的終極答案42在內存中是如何表示的(或者說怎麼獲取其字節數組)?
由於42是一個整數,根據硬件不一樣,其佔用字節大小可能爲1, 2, 4, 8等。這裏咱們限定一個整數佔用4個字節,因而問題的等價表述爲:怎樣將一個整數轉換成字節數組(本機序,4個字節)?
由於是多字節,因此要考慮字節序的問題。42不超過255,只佔用一個字節,故而其餘三個字節都是0。據此獲得結論:若是是大端序(低位字節存放在地址高位),四個字節分別是:0 0 0 42;若是是小端序,結果則是:42 0 0 0。
那怎麼知道機器的字節序呢?PHP沒有提供相關功能,也不能像C
語言直接取地址訪問字節數據。無所不能的PHP該怎麼搞定字節序,或者說完成數據向字節的轉換?
PHP應用層面,數據向字節(數組)的轉換是pack
的專場,字節(數組)向數據的轉換則是unpack
的專場。除這兩個函數,字節數組(或二進制數據)向數據的轉換幾無可能(若是有請不吝指教)。
如今咱們用pack
函數獲取42在內存中的字節數組。相關代碼以下:
function intToBytes(int $num) : string { return pack("l", $num); } function outputBytes(string $bytes) { echo "bytes: "; for ($i = 0; $i < strlen($bytes); ++ $i) { echo ord($bytes[$i]), " "; } echo PHP_EOL; } outputBytes(intToBytes(42)); // 程序輸出: bytes: 42 0 0 0
本人計算機用的英特爾的CPU,x86架構是小端序,因此程序輸出符合預期。
延伸一下,怎麼判斷機器的字節序?有了pack
函數,答案很是簡單:
function bigEndian() : bool { $data = 0x1200; $bytes = pack("s", $data); return ord($bytes[0]) === 0x12; }
調用函數便返回本機是否大端序。
上述是pack
函數簡單的使用場景,接下來分別介紹pack
和unpack
函數。
pack
和unpack
pack
函數pack
是「打包/封包」的意思。如其名,pack
函數的工做是將數據按照格式打包成字節數組。函數原型爲:
pack ( string $format [, mixed $... ] ) : string
形式上與printf
系列函數相同:第一個參數是格式字符串,其他參數是要格式化的參數。不一樣之處在於pack
函數的格式中不能出現元字符和量詞外的其餘字符,因此不須要%
符號。
上文的例子中使用了"l"和"s"兩個格式化元字符,pack
函數的元字符主要分爲三類:
a
、A
等;將數據轉成字符串,功能上與sprintf
相似,例如整數32轉換成字符串"32";h
和H
;對字節進行16進制編碼,區別在於低位仍是高位在前,功能上與dechex
等函數相似;c/s/i/l
等;將數據轉換成對應類型的字節數組,除char
類型外(暫)沒有其餘函數可替代;注意:char
和a/A
等的區別是a/A
等輸入爲字符(串),而's/S'的輸入要求是小於256的整數,輸入字符會獲得0。
量詞比較簡單:數字和""兩種。例如"i2"表示將兩個參數按照整數轉換,"c"表示後續都按照char
類型轉換。
unpack
unpack
是pack
的反向操做:將字節數組解析成有意義的數據。其函數原型爲:
unpack ( string $format , string $data [, int $offset = 0 ] ) : array
unpack
函數須要注意的是第一個參數和返回值。返回值好理解,pack
函數至關於將除格式化參數外的參數數組(想象成call_user_func_array
的參數)變成一個字節數組;unpack
作相反的事情:釋放數據,獲得輸入時的參數數組。
返回一個數組,其鍵分別是什麼呢?這即是格式化參數($format
)在pack
和unpack
的不一樣之處:unpack
應該對釋放出來的數據命名,用"/"分隔各組數據。因爲格式化參數容許有非元字符和量詞外的字符,爲了區分數據,不一樣數據間的"/"分隔符必不可少。
一個例子:
$bytes = pack("iaa*", 42, ":", "The answer to life, the universe and everything"); outputBytes($bytes); $result = unpack("inumber/acolon/a*word", $bytes); print_r($result); // 程序輸出: bytes: 42 0 0 0 58 84 104 101 32 97 110 115 119 101 114 32 116 111 32 108 105 102 101 44 32 116 104 101 32 117 110 105 118 101 114 115 101 32 97 110 100 32 101 118 101 114 121 116 104 105 110 103 Array ( [num] => 42 [colon] => : [word] => The answer to life, the universe and everything )
若是不對釋放出來的數據命名會怎麼樣?例如上例中unpack
的格式化參數爲:"i/a/a*",結果是什麼呢?其結果爲:
Array ( [1] => The answer to life, the universe and everything )
爲什麼?官方文檔上如是說:
Caution If you do not name an element, numeric indices starting from 1 are used. Be aware that if you have more than one unnamed element, some data is overwritten because the numbering restarts from 1 for each element.
翻譯過來就是:若是你不對數據命名,默認的1, 2, 3...就用來看成鍵值。若是有多組數據,每組都用一樣的下標,會致使數據覆蓋。
因此能理解 "i/a/a"* 爲什麼只剩最後一組數據了吧?
讀取圖像、word/excel文件,解析binlog、二進制ip數據庫文件等場合,pack
和unpack
幾乎必不可少。本文舉例說一下pack
和unpack
在網絡編程時協議解析的用途。
假設咱們的tcp包格式爲:前四個字節表示包大小,其他字節爲數據內容。因而客戶(發送)端的send
函數能夠長這樣:
public function send($data) { // 這裏假設$data已經作了序列化、加密等操做,是字節數組 // 計算報文長度,封裝報文 $len = strlen($data); $header = pack("L", $len); // 轉換成網絡(大端)序 $header = xxx // 封包 $binary = $header . $data; // 調用fwrite/socket_send等將數據寫入內核緩衝區 ... }
服務(接收)端根據協議解析接收到的數據流:
public function decodable($session, $buffer) { $dataLen = strlen($buffer); // 非法數據包 if ($dataLen < 4) { // 關閉鏈接、記錄ip等 .... return NOT_OK; } // 獲取前四個字節 $header = substr($buffer, 0, 4); // 轉換成主機序 $header = xxx // 解析數據長度 $len = unpack("L", $header); // 單個報文不能超過8M,例如限制上傳的圖像大小 if ($len > 8 * 1024 * 1024) { // 關閉鏈接等 return NOT_OK; } // 檢查數據包是否知足協議要求 if ($dataLen - 4 >= $len) { return OK; } // 數據未所有到達,繼續等待 return NEED_DATA; }
經過pack
和unpack
,咱們順利的處理報文協議和二進制字節流的發送和解析。
若是你用\n
做爲報文分隔符,pack
和unpack
也許用不到。但在網絡通信中直接傳遞字符畢竟少數(至關於明文傳送),大多數狀況下的二進制數據流的解析仍是要靠pack
和unpack
。
除分配內存,最重要的系統調用莫過於文件讀寫和網絡鏈接,而二者的本質操做對象都是字節流。pack
和unpack
爲PHP提供了底層字節操做的能力,在二進制數據處理中十分有用。有志於跳出web編程的PHP開發應該都要掌握這兩個函數。