日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > php >内容正文

php

PHP: 深入pack/unpack

發布時間:2023/12/15 php 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 PHP: 深入pack/unpack 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

為什么80%的碼農都做不了架構師?>>> ??

PHP作為一門為web而生的服務器端開發語言,被越來越多的公司所采用。其中不乏大公司,如騰迅、盛大、淘米、新浪等。在對性能要求比較高的項目中,PHP也逐漸演變成一門前端語言,用于訪問后端接口?;蛘卟煌椖恐g需要共享數據的時候,通常可以抽取出數據層,通過PHP來訪問。

寫在前面的話

本文介紹的是通過二進制數據包的方式通信,演示語言為PHP和Golang。PHP提供了pack/unpack函數來進行二進制打包和二進制解包。在具體講解之前,我們先來了解一些基礎知識。

什么是字節序

在不同的計算機體系結構中,對于數據(比特、字節、字)等的存儲和傳輸機制有所不同,因而引發了計算機領域中一個潛在但是又很重要的問題,即通信雙方交流的信息單元應該以什么樣的順序進行傳送。如果達不成一致的規則,計算機的通信與存儲將會無法進行。目前在各種體系的計算機中通常采用的字節存儲機制主要有兩種:大端(Big-endian)和小端(Little-endian)。這里所說的大端和小端即是字節序。

MSB和LSB

  • MSB是Most Significant Bit/Byte的首字母縮寫,通常譯為最重要的位或最重要的字節。它通常用來表示在一個bit序列(如一個byte是8個bit組成的一個序列)或一個byte序列(如word是兩個byte組成的一個序列)中對整個序列取值影響最大的那個bit/byte。

  • LSB是Least Significant Bit/Byte的首字母縮寫,通常譯為最不重要的位或最不重要的字節。它通常用來表明在一個bit序列(如一個byte是8個bit組成的一個序列)或一個byte序列(如word是兩個byte組成的一個序列)中對整個序列取值影響最小的那個bit/byte。

  • 對于一個十六進制int類型整數0x12345678來說,0x12就是MSB,0x78就是LSB。而對于0x78這個字節而言,它的二進制是01111000,那么最左邊的那個0就是MSB,最右邊的那個0就是LSB。

大端序

  • 大端序又叫網絡字節序。大端序規定高位字節在存儲時放在低地址上,在傳輸時高位字節放在流的開始;低位字節在存儲時放在高地址上,在傳輸時低位字節放在流的末尾。

小端序

  • 小端序規定高位字節在存儲時放在高地址上,在傳輸時高位字節放在流的末尾;低位字節在存儲時放在低地址上,在傳輸時低位字節放在流的開始。

網絡字節序

  • 網絡字節序是指大端序。TCP/IP都是采用網絡字節序的方式,java也是使用大端序方式存儲。

主機字節序

  • 主機字節序代表本機的字節序。一般是小端序,但也有一些是大端序。

  • 主機字節序用在協議描述中則是指小端序。

總結

  • 字節序只針對于多字節類型的數據。比如對于int類型整數0x12345678,它占有4個字節的存儲空間,存儲方式有大端(0x12, 0x34, 0x56, 0x78)和小端(0x78, 0x56, 0x34, 0x12)兩種。可以看到,在大端或小端的存儲方式中,是以字節為單位的。所以對于單字節類型的數據,不存在字節序這個說法。

pack/unpack詳解

PHP pack函數用于將其它進制的數字壓縮到位字符串之中。也就是把其它進制數字轉化為ASCII碼字符串。

格式字符翻譯

  • a -- 將字符串空白以 NULL 字符填滿

  • A -- 將字符串空白以 SPACE 字符 (空格) 填滿

  • h -- 16進制字符串,低位在前以半字節為單位

  • H -- 16進制字符串,高位在前以半字節為單位

  • c -- 有符號字符

  • C -- 無符號字符

  • s -- 有符號短整數 (16位,主機字節序)

  • S -- 無符號短整數 (16位,主機字節序)

  • n -- 無符號短整數 (16位, 大端字節序)

  • v -- 無符號短整數 (16位, 小端字節序)

  • i -- 有符號整數 (依賴機器大小及字節序)

  • I -- 無符號整數 (依賴機器大小及字節序)

  • l -- 有符號長整數 (32位,主機字節序)

  • L -- 無符號長整數 (32位,主機字節序)

  • N -- 無符號長整數 (32位, 大端字節序)

  • V -- 無符號長整數 (32位, 小端字節序)

  • f -- 單精度浮點數 (依計算機的范圍)

  • d -- 雙精度浮點數 (依計算機的范圍)

  • x -- 空字節

  • X -- 倒回一位

  • @ -- 填入 NULL 字符到絕對位置

格式字符詳解

  • pack/unpack允許使用修飾符*和數字,緊跟在格式字符之后,用于指定該格式的個數;

  • a和A都是用來打包字符串的,它們的唯一區別就是當小于定長時的填充方式。a以NULL填充,NULL事實上是'\0'的表示,代表空字節,8個位上全是0。A以空格填充,空格也即ASCII碼為32的字符。這里有一個關于填充的使用場景的例子:請求登錄的數據包規定用戶名不超過20個字節,密碼經過md5加密后是固定的32個字節。用戶名就是變長的,為了便于服務器端讀取和處理,通常會填充成定長。當然,這只是使用的方式之一,事實上還可以用變長的方式傳遞數據包,但這不在本文的探討范圍內。字符串有一點麻煩的是編碼問題,尤其是在跟不同的平臺通信時更為突出。比如在用pack進行打包字符串時,事實上是將字符內部的編碼打包進去。單字節字符就沒有問題,因為單字節在所有平臺上都是一致的。來看個例子(pack.php):

<?php $bin?=?pack("a",?"d"); echo?"output:?"?.?$bin?.?"\n"; echo?"output:?0x"?.?bin2hex($bin)?.?"\n";

$?php?-f?pack.php output:?d output:?0x64

$bin是返回的二進制字符,您可以直接輸出它,PHP知道如何處理。通過bin2hex方法將$bin轉換成十六進制可以知道,十六進制0x64表示的是字符d。對于中文字符(多字節字符)來說,通常有GBK編碼、BIG5編碼以及UTF8編碼等。比如在GBK編碼中,一個中文字符采用2個字節來表示;在UTF8編碼中,一個中文字符采用3個字節來表示。這通常需要協商采用統一的編碼,否則會由于內部的表示不一致導致無法處理。在PHP中只要將文件保存為特定的編碼格即可,其它語言可能跟操作系統相關,因此或許需要編碼轉換。本文的例子一概基于UTF8編碼。繼續來看個例子:

<?php $bin?=?pack("a3",?"中"); echo?"output:?0x"?.?bin2hex($bin)?.?"\n"; echo?"output:?"?.?chr(0xe4)?.?chr(0xb8)?.?chr(0xad)?.?"\n"; echo?"output:?"?.?$bin{0}?.?$bin{1}?.?$bin{2}?.?"\n";

$?php?-f?pack.php output:?0xe4b8ad output:?中 output:?中

您可能會覺得很奇怪,后面2個輸出是一樣的。ASCII碼表示單字節字符(其中包括英文字母、數字、英文標點符號、不可見字符以及控制字符等等),它總是小于0x80,即小于十進制的128。當在處理字符時,如果字節小于0x80,則把它當作單字節來處理,否則會繼續讀取下一個字節,這通常跟編碼有關,GBK會將2個字節當成一個字符來處理,UTF8則需要3個字節。有時候在PHP中需要做類似的處理,比如計算字符串中字符的個數(字符串可能包含單字節和多字節),strlen方法只能計算字節數,而mb_strlen需要開啟擴展。類似這樣的需求,其實很容易處理:

<?php function?mbstrlen($str) {$len?=?strlen($str);if?($len?<=?0){return?0;}$count??=?0;for?($i?=?0;?$i?<?$len;?$i++){$count++;if?(ord($str{$i})?>=?0x80){$i?+=?2;}}return?$count; }echo?"output:?"?.?mbstrlen("中國so強大!")?.?"\n";

$?php?-f?pack.php output:?7

以上代碼的實現就是利用單字節字符的ASCII碼小于0x80。至于要跳過幾個字節,這要看具體是什么編碼。接下來通過例子來看看a和A的區別:

$GOPATH/src

----pack_test

--------main.go

main.go的源碼(只是用于測試,沒有考慮細節):

package?mainimport?("fmt""net" )const?BUF_SIZE?=?20func?handleConnection(conn?net.Conn)?{defer?conn.Close()buf?:=?make([]byte,?BUF_SIZE)n,?err?:=?conn.Read(buf)if?err?!=?nil?{fmt.Printf("err:?%v\n",?err)return}fmt.Printf("\n已接收:%d個字節,數據是:'%s'\n",?n,?string(buf)) }func?main()?{ln,?err?:=?net.Listen("tcp",?":9872")if?err?!=?nil?{fmt.Printf("error:?%v\n",?err)return}for?{conn,?err?:=?ln.Accept()if?err?!=?nil?{continue}go?handleConnection(conn)} }

代碼很簡單,收到數據,然后輸出。

pack.php

<?php $host?=?"127.0.0.1"; $port?=?"9872";$socket?=?socket_create(AF_INET,?SOCK_STREAM,?SOL_TCP)or?die("Unable?to?create?socket\n");@socket_connect($socket,?$host,?$port)?or?die("Connect?error.\n");if?($err?=?socket_last_error($socket)) {socket_close($socket);die(socket_strerror($err)?.?"\n"); }$binarydata?=?pack("a20",?"中國強大"); $len?=?socket_write?($socket?,?$binarydata,?strlen($binarydata)); socket_close($socket);

$?cd?$GOPATH/src/pack_test $?go?build $?./pack_test

$?php?-f?pack.php

當執行php后,可以看到服務器端在控制臺輸出:

已接收:20個字節,數據是:'中國強大'

以上的輸出中,單引號不是數據的一部分,只是為了便于觀察。很明顯,我們打包的字符串只占12字節,a20表示20個a,您當然可以連續寫20個a,但我想您不會這么傻。如果是a*的話,則表示任意多個a。通過服務器端的輸出來看,PHP發送了20個字節過去,服務器端也接收了20個字節,但因為填充的\0是空字符,所以您不會看到有什么不一樣的地方?,F在我們將a20換成A20,代碼如下:

<?php $host?=?"127.0.0.1"; $port?=?"9872";$socket?=?socket_create(AF_INET,?SOCK_STREAM,?SOL_TCP)or?die("Unable?to?create?socket\n");@socket_connect($socket,?$host,?$port)?or?die("Connect?error.\n");if?($err?=?socket_last_error($socket)) {socket_close($socket);die(socket_strerror($err)?.?"\n"); }$binarydata?=?pack("A20",?"中國強大"); $len?=?socket_write?($socket?,?$binarydata,?strlen($binarydata)); socket_close($socket);

$?php?-f?pack.php

您會發現服務器端的輸出不一樣了:

已接收:20個字節,數據是:'中國強大????????'

是的,空格存在于數據中。這就是a和A的區別。

  • h和H的描述看起來有些奇怪。它們都是讀取十進制,以十六進制方式讀取,以半字節(4位)為單位。這聽起來有些拗口,還是以實例來說明:

<?php echo?"output:?"?.?pack("H",?0x5)?.?"\n";

$?php?-f?pack.php output:?P

首先是讀取十進制,所以0x5會轉成十進制的5,然后以半字節為單位并且以十六進制方式讀取,為了補足8位,所以需要在5后面補0,變成0x50。別忘了十六進制的一位相當于二進制的四位。0x50正好是字符P的ASCII碼。

<?php echo?"output:?"?.?chr(0x50)?.?"\n";

$?php?-f?pack.php output:?P

h和H的差別在于h是低位在前,H是高位在前,拿前面的例子來看看h的行為:

<?php $bin?=?pack("h",?0x5); echo?"output:?"?.?$bin?.?"\n"; echo?"output:?"?.?ord($bin)?.?"\n";

$?php?-f?pack.php output:? output:?5

讀取十進制的5,后面補0,變成十六進制的0x50,因為H是高位在前,所以沒有變化,而h就需要將0x50變成0x05。由于0x05是不可見字符,所以上面的字符輸出是空的。

h和H是以半字節為單位,h2和H2則表示一次讀取8位,同理h3和H3可以推導出來,但是別忘了補足8位哦!

<?php echo?"output:?"?.?pack("H",?0x47)?.?"\n";

$?php?-f?pack.php output:?p

以上的代碼中,0x47為十進制的71,因為讀取半個字節,所以變成0x7,后面補0變成0x70,則剛好是字符p的ASCII碼。如果換成是h格式化,則最終的結果是0x07,因為低位在前。

對于一次讀取多個字節,也以同樣的規則:

<?php echo?"output:?"?.?pack("H2h2",?0x47,?0x56)?.?"\n";

$?php?-f?pack.php output:?qh

0x47是十進制的71,由于使用H2格式化,所以一次讀取8位,最后變成十六進制的0x71,即字符q的ASCII碼。0x56是十進制的86,由于使用h2格式化,所以一次讀取8位,最后變成十六進制的0x86,但是由于h表示低位在前,因此0x86變成0x68,即字符h的ASCII碼。

  • c和C都表示字符,前者表示有符號字符,后者表示無符號字符。

<?php echo?"output:?"?.?pack("c",?65)?.?"\n"; echo?"output:?"?.?pack("C",?65)?.?"\n";

$?php?-f?pack.php output:?A output:?A

  • s為有符號短整數;S為無符號短整數。它們都為主機字節序,并且為16位。通常為主機字節序的格式化字符,一般只用于單機的操作,因為您無法確定主機字節序究竟是大端還是小端。當然,您一定要這么干的話,也是有辦法來獲取本機字節序是屬于大端或小端,但那樣是沒有必要的。稍后就會給出一個通過PHP來判斷字節序的例子。

<?php $bin1?=?pack("s",?345); $bin2?=?pack("S",?452); print_r(unpack("sshort1",?$bin1)); print_r(unpack("sshort2",?$bin2));

$?php?-f?pack.php Array ([short1]?=>?345 ) Array ([short2]?=>?452 )

  • n和v除了明確指定了字節序,其它行為跟s和S是一樣的。

  • i和I依賴于機器大小及字節序,很少用它們。

  • l、L、N、V跟s、S、n、v類似,除了表示的大小不同,前者都為32位,后者都為16位。

  • f、d是因為float和double與CPU無關。一般來說,編譯器是按照IEEE標準解釋的,即把float/double看作4/8個字符的數組進行解釋。因此,只要編譯器是支持IEEE浮點標準的,就不需要考慮字節順序。

  • 剩下的x、X和@用得比較少,對此不作深究。

unpack的用法

  • unpack是用來解包經過pack打包的數據包,如果成功,則返回數組。其中格式化字符和執行pack時一一對應,但是需要額外的指定一個key,用作返回數組的key。多個字段用/分隔。例如:

<?php $bin?=?@pack("a9SS",?"陳一回",?20,?1); $data?=?@unpack("a9name/sage/Sgender",?$bin);if?(is_array($data)) {print_r($data); }

$?php??-f?pack.php Array ([name]?=>?陳一回[age]?=>?20[gender]?=>?1 )

一些例子

  • 判斷大小端

<?php function?IsBigEndian() {$bin?=?pack("L",?0x12345678);$hex?=?bin2hex($bin);if?(ord(pack("H2",?$hex))?===?0x78){return?FALSE;}return?TRUE; }if?(IsBigEndian()) {echo?"大端序"; } else {echo?"小端序"; }echo?"\n";

$?php?-f?pack.php 小端序

  • 網絡通信

    比如現在要通過PHP發送數據包到服務器來登錄。在僅需要提供用戶名(最多30個字節)和密碼(md5之后固定為32字節)的情況下,可以構造如下數據包(當然這事先需要跟服務器協商好數據包的規范,本例以網絡字節序通信):

    包結構:

字段字節數說明
包頭定長每一個通信消息必須包含的內容
包體不定長根據每個通信消息的不同產生變化

其中包頭詳細內容如下:

字段
字節數類型
說明
pkg_len2
ushort整個包的長度,不超過4K
version1uchar通訊協議版本號
command_id2ushort消息命令ID
result2short請求時不起作用;請求返回時使用

當然實際中可能會涉及到各種校驗。本文為了簡單,只是列舉一下通常的工作流程及處理的方式。

登錄(執行命儲1001)

字段字節數類型說明
用戶名30uchar[30]登錄用戶名
密碼32uchar[32]登錄密碼

包頭是定長的,通過計算可知包頭占7個字節,并且包頭在包體之前。比如用戶陳一回需要登錄,密碼是123456,則代碼如下:

<?php $version????=?1; $result?????=?0; $command_id?=?1001; $username???=?"陳一回"; $password???=?md5("123456"); //?構造包體 $bin_body???=?pack("a30a32",?$username,?$password); //?包體長度 $body_len???=?strlen($bin_body); $bin_head???=?pack("nCns",?$body_len,?$version,?$command_id,?$result); $bin_data???=?$bin_head?.?$bin_body; //?發送數據 //?socket_write($socket,?$bin_data,?strlen($bin_data)); //?socket_close($socket);

服務器端通過讀取定長包頭,拿到包體長度,再讀取并解析包體。大致的過程就是這樣。當然服務器端也會返回響應包,客戶端做相應的讀取處理。


轉載于:https://my.oschina.net/goal/blog/195749

總結

以上是生活随笔為你收集整理的PHP: 深入pack/unpack的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。