PHP有兩個重要的冷門函數(shù):pack和unpack。在網(wǎng)絡編程,讀寫圖像文件等場景,這兩個函數(shù)幾乎必不可少。鑒于文件讀寫/網(wǎng)絡編程,或者說字節(jié)流處理的重要性,掌握這兩個函數(shù)是邁向高級PHP編程的基礎。
本文先介紹字節(jié)和字符的區(qū)別,說明兩個函數(shù)存在的必要性和重要性。然后介紹基本用法和使用場景,讓讀者對其有大體了解,為實際使用中奠定基礎。
字節(jié)和字符
PHP的優(yōu)勢是簡單易用,熟練運用 字符串 和 數(shù)組 相關函數(shù)就能抗住一般的需求。日常工作中多用到字符串,所以PHP開發(fā)對字符都比較熟悉,稍微資深點基本能也能弄清字符編碼。但字符的伴生概念:字節(jié),不少PHP開發(fā)并不知曉/熟悉。
這不怪他們。PHP世界里極少出現(xiàn)“字節(jié)(流)”的概念:沒有byte關鍵字(當然也沒有char),官方文檔也沒提字節(jié);沒有原生的數(shù)組支持(常用的array其實是hashtable);當然字符串(string)能表達其他語言中的字節(jié)數(shù)組(Byte Array, byte[])。
字節(jié)和字符有什么聯(lián)系和區(qū)別呢?簡單來說字節(jié)是計算機存儲和操作的最小單位,字符是人們閱讀的最小單位;字節(jié)是存儲(物理)概念,字符是邏輯概念;字節(jié)代表數(shù)據(jù)(內(nèi)涵和本質),字符代表其含義;字符由字節(jié)組成。
舉幾個例子說明兩者區(qū)別:“中國”包含2個字符,GBK編碼表示需要4個字節(jié),UTF-8編碼需要6個字節(jié);數(shù)字“1234567890”,包含10個字符,用int32類型表示只需4個字節(jié);下面的圖片占用42582個字節(jié),用字符表示是“我老婆”,只占用3個字符:

再舉一個常用的例子說明字符和字節(jié)的區(qū)別。開發(fā)中我們常用md5算法獲取數(shù)據(jù)的哈希值,算法返回一個128位(bit)的數(shù)據(jù)(16個字節(jié))。為方便查看其值,人們約定成俗地用十六進制表示,結果就是我們熟知的32位長度的字符串(不區(qū)分大小寫)。32長度字符串不是md5算法的必然結果,16字節(jié)數(shù)據(jù)才是其本質。如果你愿意,可以用一個小于2^128的數(shù)字表示哈希結果,也可以將16字節(jié)base64編碼后作為其結果。所以常用的32位哈希值與md5返回的16字節(jié)關系為:一個是字符表示,另一個則是其本質(字符數(shù)組)(PHP的md5函數(shù)第二個參數(shù)值為true便可得到16字節(jié)數(shù)據(jù),或hash函數(shù)第三個參數(shù)為true)。
相關概念還有字節(jié)序、字符編碼等,本文不做展開。
引言
PHP中專門處理字符串的函數(shù)有幾十個,加上正則、時間等函數(shù),字符串處理的函數(shù)不下百個。相比之下字節(jié)處理門庭冷落,相關函數(shù)寥寥無幾。除了常用的ord/chr,哈希加密函數(shù)返回的原始字節(jié)、openssl庫的openssl_random_pseudo_bytes等函數(shù)真正處理或返回 字節(jié)外,最重要的兩個字節(jié)處理函數(shù)是pack和unpack。
本節(jié)從問題引出pack函數(shù)的使用。
問題考慮一個簡單的問題:宇宙的終極答案42在內(nèi)存中是如何表示的(或者說怎么獲取其字節(jié)數(shù)組)?
因為42是一個整數(shù),根據(jù)硬件不同,其占用字節(jié)大小可能為1, 2, 4, 8等。這里我們限定一個整數(shù)占用4個字節(jié),于是問題的等價表述為:怎樣將一個整數(shù)轉換成字節(jié)數(shù)組(本機序,4個字節(jié))?
分析因為是多字節(jié),所以要考慮字節(jié)序的問題。42不超過255,只占用一個字節(jié),故而其他三個字節(jié)都是0。據(jù)此得到結論:如果是大端序(低位字節(jié)存放在地址高位),四個字節(jié)分別是:0 0 0 42;如果是小端序,結果則是:42 0 0 0。
那怎么知道機器的字節(jié)序呢?PHP沒有提供相關功能,也不能像C語言直接取地址訪問字節(jié)數(shù)據(jù)。無所不能的PHP該怎么搞定字節(jié)序,或者說完成數(shù)據(jù)向字節(jié)的轉換?
方案PHP應用層面,數(shù)據(jù)向字節(jié)(數(shù)組)的轉換是pack的專場,字節(jié)(數(shù)組)向數(shù)據(jù)的轉換則是unpack的專場。除這兩個函數(shù),字節(jié)數(shù)組(或二進制數(shù)據(jù))向數(shù)據(jù)的轉換幾無可能(如果有請不吝指教)。
現(xiàn)在我們用pack函數(shù)獲取42在內(nèi)存中的字節(jié)數(shù)組。相關代碼如下:
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架構是小端序,所以程序輸出符合預期。
延伸一下,怎么判斷機器的字節(jié)序?有了pack函數(shù),答案非常簡單:
function bigEndian() : bool { $data = 0x1200; $bytes = pack( s , $data); return ord($bytes[0]) === 0x12;}調用函數(shù)便返回本機是否大端序。
上述是pack函數(shù)簡單的使用場景,接下來分別介紹pack和unpack函數(shù)。
pack和unpack
pack函數(shù)
pack是“打包/封包”的意思。如其名,pack函數(shù)的工作是將數(shù)據(jù)按照格式打包成字節(jié)數(shù)組。函數(shù)原型為:
pack ( string $format [, mixed $... ] ) : string形式上與printf系列函數(shù)相同:第一個參數(shù)是格式字符串,其余參數(shù)是要格式化的參數(shù)。不同之處在于pack函數(shù)的格式中不能出現(xiàn)元字符和量詞外的其他字符,所以不需要%符號。
上文的例子中使用了 l 和 s 兩個格式化元字符,pack函數(shù)的元字符主要分為三類:
字符串:a、A等;將數(shù)據(jù)轉成字符串,功能上與sprintf類似,例如整數(shù)32轉換成字符串 32 ;
字節(jié):h和H;對字節(jié)進行16進制編碼,區(qū)別在于低位還是高位在前,功能上與dechex等函數(shù)類似;
char/short/int/long/float/double六種基本類型:c/s/i/l等;將數(shù)據(jù)轉換成對應類型的字節(jié)數(shù)組,除char類型外(暫)沒有其他函數(shù)可替代;
注意:char和a/A等的區(qū)別是a/A等輸入為字符(串),而 s/S 的輸入要求是小于256的整數(shù),輸入字符會得到0。
量詞比較簡單:數(shù)字和 兩種。例如 i2 表示將兩個參數(shù)按照整數(shù)轉換, c 表示后續(xù)都按照char類型轉換。
unpack
unpack是pack的反向操作:將字節(jié)數(shù)組解析成有意義的數(shù)據(jù)。其函數(shù)原型為:
unpack ( string $format , string $data [, int $offset = 0 ] ) : arrayunpack函數(shù)需要注意的是第一個參數(shù)和返回值。返回值好理解,pack函數(shù)相當于將除格式化參數(shù)外的參數(shù)數(shù)組(想象成call_user_func_array的參數(shù))變成一個字節(jié)數(shù)組;unpack做相反的事情:釋放數(shù)據(jù),得到輸入時的參數(shù)數(shù)組。
返回一個數(shù)組,其鍵分別是什么呢?這便是格式化參數(shù)($format)在pack和unpack的不同之處:unpack應該對釋放出來的數(shù)據(jù)命名,用 / 分隔各組數(shù)據(jù)。由于格式化參數(shù)允許有非元字符和量詞外的字符,為了區(qū)分數(shù)據(jù),不同數(shù)據(jù)間的 / 分隔符必不可少。
一個例子:
$bytes = pack( iaa* , 42, : , The answer to life, the universe and everything outputBytes($bytes);
// 程序輸出: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 103Array [num] = 42 [colon] = : [word] = The answer to life, the universe and everything)
如果不對釋放出來的數(shù)據(jù)命名會怎么樣?例如上例中unpack的格式化參數(shù)為: 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.翻譯過來就是:如果你不對數(shù)據(jù)命名,默認的1, 2, 3...就用來當作鍵值。如果有多組數(shù)據(jù),每組都用同樣的下標,會導致數(shù)據(jù)覆蓋。
所以能理解 i/a/a* 為何只剩最后一組數(shù)據(jù)了吧?
應用場景讀取圖像、word/excel文件,解析binlog、二進制ip數(shù)據(jù)庫文件等場合,pack和unpack幾乎必不可少。本文舉例說一下pack和unpack在網(wǎng)絡編程時協(xié)議解析的用途。
假設我們的tcp包格式為:前四個字節(jié)表示包大小,其余字節(jié)為數(shù)據(jù)內(nèi)容。于是客戶(發(fā)送)端的send函數(shù)可以長這樣:
html' target='_blank'>public function send($data) { // 這里假設$data已經(jīng)做了序列化、加密等操作,是字節(jié)數(shù)組 // 計算報文長度,封裝報文 $len = strlen($data); $header = pack( L , $len); // 轉換成網(wǎng)絡(大端)序 $header = xxx // 封包 $binary = $header . $data; // 調用fwrite/socket_send等將數(shù)據(jù)寫入內(nèi)核緩沖區(qū)}
服務(接收)端根據(jù)協(xié)議解析接收到的數(shù)據(jù)流:
public function decodable($session, $buffer) { $dataLen = strlen($buffer); // 非法數(shù)據(jù)包 if ($dataLen 4) { // 關閉連接、記錄ip等 .... return NOT_OK; // 獲取前四個字節(jié) $header = substr($buffer, 0, 4); // 轉換成主機序 $header = xxx // 解析數(shù)據(jù)長度 $len = unpack( L , $header); // 單個報文不能超過8M,例如限制上傳的圖像大小 if ($len 8 * 1024 * 1024) { // 關閉連接等 return NOT_OK; // 檢查數(shù)據(jù)包是否滿足協(xié)議要求 if ($dataLen - 4 = $len) { return OK; // 數(shù)據(jù)未全部到達,繼續(xù)等待 return NEED_DATA;}通過pack和unpack,我們順利的處理報文協(xié)議和二進制字節(jié)流的發(fā)送和解析。
如果你用/n作為報文分隔符,pack和unpack也許用不到。但在網(wǎng)絡通訊中直接傳遞字符畢竟少數(shù)(相當于明文傳送),大多數(shù)情況下的二進制數(shù)據(jù)流的解析還是要靠pack和unpack。
總結
除分配內(nèi)存,最重要的系統(tǒng)調用莫過于文件讀寫和網(wǎng)絡連接,而兩者的本質操作對象都是字節(jié)流。pack和unpack為PHP提供了底層字節(jié)操作的能力,在二進制數(shù)據(jù)處理中十分有用。有志于跳出web編程的PHP開發(fā)應該都要掌握這兩個函數(shù)。
以上就是PHP中的pack函數(shù)和unpack函數(shù)的詳細介紹(附代碼)的詳細內(nèi)容,PHP教程
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。
新聞熱點
疑難解答