摘要:鑒于文件讀寫(xiě)網(wǎng)絡(luò)編程,或者說(shuō)字節(jié)流處理的重要性,掌握這兩個(gè)函數(shù)是邁向高級(jí)編程的基礎(chǔ)。相比之下字節(jié)處理門(mén)庭冷落,相關(guān)函數(shù)寥寥無(wú)幾。上述是函數(shù)簡(jiǎn)單的使用場(chǎng)景,接下來(lái)分別介紹和函數(shù)。如其名,函數(shù)的工作是將數(shù)據(jù)按照格式打包成字節(jié)數(shù)組。
轉(zhuǎn)載請(qǐng)注明文章出處:https://tlanyan.me/php-pack-a...
PHP有兩個(gè)重要的冷門(mén)函數(shù):pack和unpack。在網(wǎng)絡(luò)編程,讀寫(xiě)圖像文件等場(chǎng)景,這兩個(gè)函數(shù)幾乎必不可少。鑒于文件讀寫(xiě)/網(wǎng)絡(luò)編程,或者說(shuō)字節(jié)流處理的重要性,掌握這兩個(gè)函數(shù)是邁向高級(jí)PHP編程的基礎(chǔ)。
本文先介紹字節(jié)和字符的區(qū)別,說(shuō)明兩個(gè)函數(shù)存在的必要性和重要性。然后介紹基本用法和使用場(chǎng)景,讓讀者對(duì)其有大體了解,為實(shí)際使用中奠定基礎(chǔ)。
字節(jié)和字符PHP的優(yōu)勢(shì)是簡(jiǎn)單易用,熟練運(yùn)用 字符串 和 數(shù)組 相關(guān)函數(shù)就能抗住一般的需求。日常工作中多用到字符串,所以PHP開(kāi)發(fā)對(duì)字符都比較熟悉,稍微資深點(diǎn)基本能也能弄清字符編碼。但字符的伴生概念:字節(jié),不少PHP開(kāi)發(fā)并不知曉/熟悉。
這不怪他們。PHP世界里極少出現(xiàn)“字節(jié)(流)”的概念:沒(méi)有byte關(guān)鍵字(當(dāng)然也沒(méi)有char),官方文檔也沒(méi)提字節(jié);沒(méi)有原生的數(shù)組支持(常用的array其實(shí)是hashtable);當(dāng)然字符串(string)能表達(dá)其他語(yǔ)言中的字節(jié)數(shù)組(Byte Array, byte[])。
字節(jié)和字符有什么聯(lián)系和區(qū)別呢?簡(jiǎn)單來(lái)說(shuō)字節(jié)是計(jì)算機(jī)存儲(chǔ)和操作的最小單位,字符是人們閱讀的最小單位;字節(jié)是存儲(chǔ)(物理)概念,字符是邏輯概念;字節(jié)代表數(shù)據(jù)(內(nèi)涵和本質(zhì)),字符代表其含義;字符由字節(jié)組成。
舉幾個(gè)例子說(shuō)明兩者區(qū)別:“中國(guó)”包含2個(gè)字符,GBK編碼表示需要4個(gè)字節(jié),UTF-8編碼需要6個(gè)字節(jié);數(shù)字“1234567890”,包含10個(gè)字符,用int32類(lèi)型表示只需4個(gè)字節(jié);下面的圖片占用42582個(gè)字節(jié),用字符表示是“我老婆”,只占用3個(gè)字符:
再舉一個(gè)常用的例子說(shuō)明字符和字節(jié)的區(qū)別。開(kāi)發(fā)中我們常用md5算法獲取數(shù)據(jù)的哈希值,算法返回一個(gè)128位(bit)的數(shù)據(jù)(16個(gè)字節(jié))。為方便查看其值,人們約定成俗地用十六進(jìn)制表示,結(jié)果就是我們熟知的32位長(zhǎng)度的字符串(不區(qū)分大小寫(xiě))。32長(zhǎng)度字符串不是md5算法的必然結(jié)果,16字節(jié)數(shù)據(jù)才是其本質(zhì)。如果你愿意,可以用一個(gè)小于2^128的數(shù)字表示哈希結(jié)果,也可以將16字節(jié)base64編碼后作為其結(jié)果。所以常用的32位哈希值與md5返回的16字節(jié)關(guān)系為:一個(gè)是字符表示,另一個(gè)則是其本質(zhì)(字符數(shù)組)(PHP的md5函數(shù)第二個(gè)參數(shù)值為true便可得到16字節(jié)數(shù)據(jù),或hash函數(shù)第三個(gè)參數(shù)為true)。
相關(guān)概念還有字節(jié)序、字符編碼等,本文不做展開(kāi)。感興趣的讀者可參考本人之前的博客“文件和字符編碼”或相關(guān)材料。
引言PHP中專(zhuān)門(mén)處理字符串的函數(shù)有幾十個(gè),加上正則、時(shí)間等函數(shù),字符串處理的函數(shù)不下百個(gè)。相比之下字節(jié)處理門(mén)庭冷落,相關(guān)函數(shù)寥寥無(wú)幾。除了常用的ord/chr,哈希加密函數(shù)返回的原始字節(jié)、openssl庫(kù)的openssl_random_pseudo_bytes等函數(shù)真正處理或返回 字節(jié)外,最重要的兩個(gè)字節(jié)處理函數(shù)是pack和unpack。
本節(jié)從問(wèn)題引出pack函數(shù)的使用。
問(wèn)題考慮一個(gè)簡(jiǎn)單的問(wèn)題:宇宙的終極答案42在內(nèi)存中是如何表示的(或者說(shuō)怎么獲取其字節(jié)數(shù)組)?
因?yàn)?2是一個(gè)整數(shù),根據(jù)硬件不同,其占用字節(jié)大小可能為1, 2, 4, 8等。這里我們限定一個(gè)整數(shù)占用4個(gè)字節(jié),于是問(wèn)題的等價(jià)表述為:怎樣將一個(gè)整數(shù)轉(zhuǎn)換成字節(jié)數(shù)組(本機(jī)序,4個(gè)字節(jié))?
分析因?yàn)槭嵌嘧止?jié),所以要考慮字節(jié)序的問(wèn)題。42不超過(guò)255,只占用一個(gè)字節(jié),故而其他三個(gè)字節(jié)都是0。據(jù)此得到結(jié)論:如果是大端序(低位字節(jié)存放在地址高位),四個(gè)字節(jié)分別是:0 0 0 42;如果是小端序,結(jié)果則是:42 0 0 0。
那怎么知道機(jī)器的字節(jié)序呢?PHP沒(méi)有提供相關(guān)功能,也不能像C語(yǔ)言直接取地址訪問(wèn)字節(jié)數(shù)據(jù)。無(wú)所不能的PHP該怎么搞定字節(jié)序,或者說(shuō)完成數(shù)據(jù)向字節(jié)的轉(zhuǎn)換?
方案PHP應(yīng)用層面,數(shù)據(jù)向字節(jié)(數(shù)組)的轉(zhuǎn)換是pack的專(zhuān)場(chǎng),字節(jié)(數(shù)組)向數(shù)據(jù)的轉(zhuǎn)換則是unpack的專(zhuān)場(chǎng)。除這兩個(gè)函數(shù),字節(jié)數(shù)組(或二進(jìn)制數(shù)據(jù))向數(shù)據(jù)的轉(zhuǎn)換幾無(wú)可能(如果有請(qǐng)不吝指教)。
現(xiàn)在我們用pack函數(shù)獲取42在內(nèi)存中的字節(jié)數(shù)組。相關(guān)代碼如下:
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
本人計(jì)算機(jī)用的英特爾的CPU,x86架構(gòu)是小端序,所以程序輸出符合預(yù)期。
延伸一下,怎么判斷機(jī)器的字節(jié)序?有了pack函數(shù),答案非常簡(jiǎn)單:
function bigEndian() : bool { $data = 0x1200; $bytes = pack("s", $data); return ord($bytes[0]) === 0x12; }
調(diào)用函數(shù)便返回本機(jī)是否大端序。
上述是pack函數(shù)簡(jiǎn)單的使用場(chǎng)景,接下來(lái)分別介紹pack和unpack函數(shù)。
pack和unpack pack函數(shù)pack是“打包/封包”的意思。如其名,pack函數(shù)的工作是將數(shù)據(jù)按照格式打包成字節(jié)數(shù)組。函數(shù)原型為:
pack ( string $format [, mixed $... ] ) : string
形式上與printf系列函數(shù)相同:第一個(gè)參數(shù)是格式字符串,其余參數(shù)是要格式化的參數(shù)。不同之處在于pack函數(shù)的格式中不能出現(xiàn)元字符和量詞外的其他字符,所以不需要%符號(hào)。
上文的例子中使用了"l"和"s"兩個(gè)格式化元字符,pack函數(shù)的元字符主要分為三類(lèi):
字符串:a、A等;將數(shù)據(jù)轉(zhuǎn)成字符串,功能上與sprintf類(lèi)似,例如整數(shù)32轉(zhuǎn)換成字符串"32";
字節(jié):h和H;對(duì)字節(jié)進(jìn)行16進(jìn)制編碼,區(qū)別在于低位還是高位在前,功能上與dechex等函數(shù)類(lèi)似;
char/short/int/long/float/double六種基本類(lèi)型:c/s/i/l等;將數(shù)據(jù)轉(zhuǎn)換成對(duì)應(yīng)類(lèi)型的字節(jié)數(shù)組,除char類(lèi)型外(暫)沒(méi)有其他函數(shù)可替代;
注意:char和a/A等的區(qū)別是a/A等輸入為字符(串),而"s/S"的輸入要求是小于256的整數(shù),輸入字符會(huì)得到0。
量詞比較簡(jiǎn)單:數(shù)字和""兩種。例如"i2"表示將兩個(gè)參數(shù)按照整數(shù)轉(zhuǎn)換,"c"表示后續(xù)都按照char類(lèi)型轉(zhuǎn)換。
unpackunpack是pack的反向操作:將字節(jié)數(shù)組解析成有意義的數(shù)據(jù)。其函數(shù)原型為:
unpack ( string $format , string $data [, int $offset = 0 ] ) : array
unpack函數(shù)需要注意的是第一個(gè)參數(shù)和返回值。返回值好理解,pack函數(shù)相當(dāng)于將除格式化參數(shù)外的參數(shù)數(shù)組(想象成call_user_func_array的參數(shù))變成一個(gè)字節(jié)數(shù)組;unpack做相反的事情:釋放數(shù)據(jù),得到輸入時(shí)的參數(shù)數(shù)組。
返回一個(gè)數(shù)組,其鍵分別是什么呢?這便是格式化參數(shù)($format)在pack和unpack的不同之處:unpack應(yīng)該對(duì)釋放出來(lái)的數(shù)據(jù)命名,用"/"分隔各組數(shù)據(jù)。由于格式化參數(shù)允許有非元字符和量詞外的字符,為了區(qū)分?jǐn)?shù)據(jù),不同數(shù)據(jù)間的"/"分隔符必不可少。
一個(gè)例子:
$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 )
如果不對(duì)釋放出來(lái)的數(shù)據(jù)命名會(huì)怎么樣?例如上例中unpack的格式化參數(shù)為:"i/a/a*",結(jié)果是什么呢?其結(jié)果為:
Array ( [1] => The answer to life, the universe and everything )
為何?官方文檔上如是說(shuō):
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.
翻譯過(guò)來(lái)就是:如果你不對(duì)數(shù)據(jù)命名,默認(rèn)的1, 2, 3...就用來(lái)當(dāng)作鍵值。如果有多組數(shù)據(jù),每組都用同樣的下標(biāo),會(huì)導(dǎo)致數(shù)據(jù)覆蓋。
所以能理解 "i/a/a*" 為何只剩最后一組數(shù)據(jù)了吧?
應(yīng)用場(chǎng)景讀取圖像、word/excel文件,解析binlog、二進(jìn)制ip數(shù)據(jù)庫(kù)文件等場(chǎng)合,pack和unpack幾乎必不可少。本文舉例說(shuō)一下pack和unpack在網(wǎng)絡(luò)編程時(shí)協(xié)議解析的用途。
假設(shè)我們的tcp包格式為:前四個(gè)字節(jié)表示包大小,其余字節(jié)為數(shù)據(jù)內(nèi)容。于是客戶(hù)(發(fā)送)端的send函數(shù)可以長(zhǎng)這樣:
public function send($data) { // 這里假設(shè)$data已經(jīng)做了序列化、加密等操作,是字節(jié)數(shù)組 // 計(jì)算報(bào)文長(zhǎng)度,封裝報(bào)文 $len = strlen($data); $header = pack("L", $len); // 轉(zhuǎn)換成網(wǎng)絡(luò)(大端)序 $header = xxx // 封包 $binary = $header . $data; // 調(diào)用fwrite/socket_send等將數(shù)據(jù)寫(xiě)入內(nèi)核緩沖區(qū) ... }
服務(wù)(接收)端根據(jù)協(xié)議解析接收到的數(shù)據(jù)流:
public function decodable($session, $buffer) { $dataLen = strlen($buffer); // 非法數(shù)據(jù)包 if ($dataLen < 4) { // 關(guān)閉連接、記錄ip等 .... return NOT_OK; } // 獲取前四個(gè)字節(jié) $header = substr($buffer, 0, 4); // 轉(zhuǎn)換成主機(jī)序 $header = xxx // 解析數(shù)據(jù)長(zhǎng)度 $len = unpack("L", $header); // 單個(gè)報(bào)文不能超過(guò)8M,例如限制上傳的圖像大小 if ($len > 8 * 1024 * 1024) { // 關(guān)閉連接等 return NOT_OK; } // 檢查數(shù)據(jù)包是否滿(mǎn)足協(xié)議要求 if ($dataLen - 4 >= $len) { return OK; } // 數(shù)據(jù)未全部到達(dá),繼續(xù)等待 return NEED_DATA; }
通過(guò)pack和unpack,我們順利的處理報(bào)文協(xié)議和二進(jìn)制字節(jié)流的發(fā)送和解析。
如果你用 作為報(bào)文分隔符,pack和unpack也許用不到。但在網(wǎng)絡(luò)通訊中直接傳遞字符畢竟少數(shù)(相當(dāng)于明文傳送),大多數(shù)情況下的二進(jìn)制數(shù)據(jù)流的解析還是要靠pack和unpack。
總結(jié)除分配內(nèi)存,最重要的系統(tǒng)調(diào)用莫過(guò)于文件讀寫(xiě)和網(wǎng)絡(luò)連接,而兩者的本質(zhì)操作對(duì)象都是字節(jié)流。pack和unpack為PHP提供了底層字節(jié)操作的能力,在二進(jìn)制數(shù)據(jù)處理中十分有用。有志于跳出web編程的PHP開(kāi)發(fā)應(yīng)該都要掌握這兩個(gè)函數(shù)。
參考文件和字符編碼
PHP Manual: pack
PHP Manual: unpack
Handling binary data in PHP with pack() and unpack()
PHP: 深入pack/unpack
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/30160.html
摘要:中有兩個(gè)函數(shù)和,很多在實(shí)際項(xiàng)目中從來(lái)沒(méi)有使用過(guò),甚至也不知道這兩個(gè)方法是用來(lái)干嘛的。比如和分別對(duì)應(yīng)的二進(jìn)制表示為。主機(jī)字節(jié)序表示當(dāng)年機(jī)器的字節(jié)序也就是網(wǎng)絡(luò)字節(jié)序是確定的,而主機(jī)字節(jié)序是依機(jī)器確定的,一般為小端字節(jié)序。 PHP中有兩個(gè)函數(shù)pack和unpack,很多PHPer在實(shí)際項(xiàng)目中從來(lái)沒(méi)有使用過(guò),甚至也不知道這兩個(gè)方法是用來(lái)干嘛的。這篇文章來(lái)為大家介紹一下它倆到底是用來(lái)干啥的。 p...
摘要:浮點(diǎn)數(shù)在計(jì)算機(jī)中是根據(jù)二進(jìn)制浮點(diǎn)數(shù)算數(shù)標(biāo)準(zhǔn)儲(chǔ)存的。尤其在我們?nèi)粘9ぷ髦?,不要比較浮點(diǎn)數(shù)的大小,如果需要精確的比較計(jì)算,請(qǐng)使用系列函數(shù)。還有一點(diǎn),浮點(diǎn)數(shù)不準(zhǔn)確和沒(méi)有任何關(guān)系,不背這個(gè)鍋。 大家在日常開(kāi)發(fā)中,必然使用過(guò)浮點(diǎn)數(shù),也會(huì)發(fā)現(xiàn)浮點(diǎn)數(shù)不是精確的,那究竟是什么原因造成的呢? 奇怪的結(jié)果 var_dump((1-0.9) == 0.1); //輸出:bool(false) 很奇怪吧!1-...
摘要:服務(wù)端可自定義方法供客戶(hù)端遠(yuǎn)程調(diào)用服務(wù)端遠(yuǎn)程調(diào)用函數(shù)參數(shù)順序數(shù)量變化不會(huì)導(dǎo)致客戶(hù)端服務(wù)端版本不兼容問(wèn)題支持多種傳輸協(xié)議支持多種通訊方式阻塞非阻塞阻塞非阻塞等支持自定義傳輸協(xié)議引入接口高冷一定要高冷設(shè)計(jì)一定要高冷傳輸相關(guān)服務(wù)端非阻塞通訊端口 Feature 服務(wù)端可自定義方法供客戶(hù)端遠(yuǎn)程調(diào)用 服務(wù)端遠(yuǎn)程調(diào)用函數(shù)參數(shù)(順序,數(shù)量)變化, 不會(huì)導(dǎo)致客戶(hù)端服務(wù)端版本不兼容問(wèn)題 支持多種傳輸協(xié)...
摘要:函數(shù)事件循環(huán)在事件循環(huán)時(shí),如果使用的是消息隊(duì)列,那么就不斷的調(diào)用從消息隊(duì)列中取出數(shù)據(jù)。獲取后的數(shù)據(jù)調(diào)用回調(diào)函數(shù)消費(fèi)消息之后,向中發(fā)送空數(shù)據(jù),告知進(jìn)程已消費(fèi),并且關(guān)閉新連接。 swManager_start 創(chuàng)建進(jìn)程流程 task_worker 進(jìn)程的創(chuàng)建可以分為三個(gè)步驟:swServer_create_task_worker 申請(qǐng)所需的內(nèi)存、swTaskWorker_init 初始化...
摘要:本文主要介紹了在框架中使用實(shí)現(xiàn)簡(jiǎn)單服務(wù)器的過(guò)程。在網(wǎng)絡(luò)通信中,需要發(fā)送二進(jìn)制流數(shù)據(jù)函數(shù)負(fù)責(zé)數(shù)據(jù)組包,即將數(shù)據(jù)按照規(guī)定的傳輸協(xié)議組合起來(lái)函數(shù)負(fù)責(zé)數(shù)據(jù)拆包,即按照規(guī)定的協(xié)議將數(shù)據(jù)拆分開(kāi)來(lái)。不多說(shuō),具體實(shí)現(xiàn)代碼咱們來(lái)看一下。 本文主要介紹了在tornado框架中,使用tcpserver,tcpclient,struct.pack(),struct.unpack實(shí)現(xiàn)簡(jiǎn)單echo服務(wù)器的過(guò)程。 ...
閱讀 2745·2023-04-26 02:44
閱讀 9977·2021-11-22 14:44
閱讀 2185·2021-09-27 13:36
閱讀 2766·2021-09-08 10:43
閱讀 759·2019-08-30 15:56
閱讀 1451·2019-08-30 15:55
閱讀 2940·2019-08-28 18:12
閱讀 2898·2019-08-26 13:50