首頁  >  文章  >  後端開發  >  PHP中的pack函數和unpack函數的詳細介紹(附程式碼)

PHP中的pack函數和unpack函數的詳細介紹(附程式碼)

不言
不言轉載
2019-02-25 09:58:113702瀏覽

這篇文章帶給大家的內容是關於PHP中的pack函數和unpack函數的詳細介紹(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

PHP有兩個重要的冷門函數:packunpack。在網路編程,讀寫圖像檔案等場景,這兩個函數幾乎必不可少。鑑於文件讀寫/網絡編程,或說字節流處理的重要性,掌握這兩個函數是邁向高級PHP編程的基礎。

本文先介紹位元組字元的區別,說明兩個函數存在的必要性和重要性。接著介紹基本用法和使用場景,讓讀者對其有大體了解,為實際使用中奠定基礎。

位元組和字元

PHP的優點是簡單易用,熟練運用 字串 和 陣列 相關函數就能抗住一般的需求。日常工作多用到字串,所以PHP開發對字元都比較熟悉,稍微資深點基本能也能弄清楚字元編碼。但字符的伴生概念:字節,不少PHP開發並不知曉/熟悉。

這不怪他們。 PHP世界裡極少出現「位元組(流)」的概念:沒有byte關鍵字(當然也沒有char),官方文件也沒提位元組;沒有原生的陣列支援(常用的array其實是hashtable);當然字串(string)能表達其他語言中的位元組數組(Byte Array, byte[])。

位元組和字元有什麼連結和差別呢?簡單來說位元組是電腦儲存和操作的最小單位,字元是人們閱讀的最小單位;位元組是儲存(物理)概念,字元是邏輯概念;位元組代表資料(內涵和本質),字元代表其含義;字元由位元組組成。

舉幾個例子說明兩者差異:「中國」包含2個字符,GBK編碼表示需要4個位元組,UTF-8編碼需要6個位元組;數字“1234567890”,包含10個字符,用int32類型表示只需4個位元組;下面的圖片佔用42582個字節,用字符表示是“我老婆”,只佔用3個字符:

PHP中的pack函數和unpack函數的詳細介紹(附程式碼)

再舉一個常用的例子來說明字元和位元組的差別。開發中我們常用md5演算法取得資料的雜湊值,演算法回傳一個128位元(bit)的資料(16個位元組)。為方便查看其值,人們約定成俗地用十六進位表示,結果就是我們熟知的32位元長度的字串(不區分大小寫)。 32長度字串不是md5演算法的必然結果,16位元組資料才是其本質。如果你願意,可以用一個小於2^128的數字表示雜湊結果,也可以將16位元組base64編碼後作為其結果。所以常用的32位元雜湊值與md5回傳的16位元組關係為:一個是字元表示,另一個則是其本質(字元陣列)(PHP的md5函數第二個參數值為true便可得到16字節數據,或hash函數第三個參數為true)。

相關概念還有字節序、字元編碼等,本文不做展開。

引言

PHP中專門處理字串的函數有幾十個,加上正規、時間等函數,字串處理的函數不下百個。相比之下位元組處理門庭冷落,相關函數寥寥無幾。除了常用的ord/chr,雜湊加密函數傳回的原始位元組、openssl函式庫的openssl_random_pseudo_bytes等函數真正處理或傳回 位元組外,最重要的兩個位元組處理函數是packunpack

本節從問題引出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函数的元字符主要分为三类:

  1. 字符串:aA等;将数据转成字符串,功能上与sprintf类似,例如整数32转换成字符串"32";

  2. 字节:hH;对字节进行16进制编码,区别在于低位还是高位在前,功能上与dechex等函数类似;

  3. char/short/int/long/float/double六种基本类型: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数据库文件等场合,packunpack几乎必不可少。本文举例说一下packunpack在网络编程时协议解析的用途。

假设我们的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。

总结

除分配內存,最重要的系統呼叫莫過於檔案讀寫和網路連接,而兩者的本質操作物件都是位元組流。 packunpack為PHP提供了底層位元組操作的能力,在二進位資料處理中十分有用。有志於跳出web程式設計的PHP開發應該都要掌握這兩個函數。

#

以上是PHP中的pack函數和unpack函數的詳細介紹(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除