hex2bin / bin2hex / pack / unpack 的理解及應用

文本文件 / 二進制文件 / 二進制bit流

計算機在存儲或傳輸數據時都是以 bit 流的形式(二進制),文本文件和二進制文件的主要區別就是在於文本文件是有字符集的,ascii/utf8/utf16 等,讀取時會將二進制流解碼成對應的字符集字符。而二進制文件則簡單的將數據做爲二進制流處理,使用文本編輯器打開時,ascii解碼,1byte 1byte 的處理並,有的落在ascii可打印字符中的就顯示,沒有落在其中的就是咱們看到的亂碼了。php

例如編程

存儲segmentfault

ascii 文本數據 hello 存儲的二進制流以下
存儲的是每一個字符的 ASCII 碼
01101000 01100101 01101100 01101100 01101111

讀取socket

按 ascii 文本文件讀取
會依次讀取 1byte 而後輸出 ascii 碼錶對應的字符
爲了便於比較咱們這裏讀取 4byte 的數據
01101000 01100101 01101100 01101100
h--------e--------l--------l-------
按二進制流文件讀取

能夠讀取 4byte 做爲 uint 解析
二進制:01101000 01100101 01101100 01101100
十進制:|------------1751477356-----------|

能夠讀取 2byte 做爲 usint 解析
二進制:01101000 01100101 01101100 01101100
十進制:|-----26725-----| |-----27756-----|

因此,文本文件和二進制文件的不一樣之處是解析方式不一樣,文本文件須要按照自身字符集,截取相應的字節長度讀取,ascii 1byte, utf8 英文 1byte/中文 3byte, unicode 2byte 的方式去一段段的讀取解析,二進制文件的話並無表徵性的字符集,你能夠按本身的須要解析,好比第1 byte位表明的什麼(unsigned short int / short int / ascii?),第2~3 byte位表明的什麼,第n~k byte位表明的什麼。tcp

由於 ascii 的 char 和 int 的存儲方式本質一致的(C語言裏 char 類型本質就是 int),因此用多字節可能更好理解。編輯器

<?php
// uft8 存儲中文字符使用 3byte 這裏咱們將其拆成字節碼後分別獲取對應的二進制
foreach (str_split("我") as $key => $chr) {
    echo sprintf("%08d", decbin(ord($char))) . ' ';
}
// result
11100110 10001000 10010001

因此,若是咱們使用 utf8 字符集處理 11100110 10001000 10010001時,咱們獲得的是 「我」。ui

若是咱們將其做爲二進制文件處理的話獲得的結果是int(15108241)數值。spa

<?php
// 將二進制字符串轉爲對應的十進制
echo bindec('111001101000100010010001');
//result
int(15108241)

hex2bin / bin2hex

其實這倆貨讓我迷惑了好久,很長一段時間我都不明白他倆到底有什麼用,輸出結果不難理解,還被同事的代碼帶過節奏,由於我看到他常常在發送數據前經過 bin2hex 轉化一下再發送,我慣性的認爲難不成能夠壓縮數據量?code

而這一次,我很刻意的將 hex2bin寫在了 bin2hex的前面。ci

hex2bin 打包

hex2bin(hexString)的做用是將字符串做爲十六進制的模式進行處理,如何處理,"68656c6c6f"會被處理成"68" "65" "6c" "6c" "6f",而後轉換成對應的二進制數值,"68"(注意是字符串 2bytes)轉爲二進制數值是 01101000(注意是數值 1byte),輸出至終端其實就是h(1bytes)的,依次處理後,咱們成功的將10bytes的字符串"68656c6c6f"轉換成了5bytes的字符串"hello",只不過"hello"並非咱們真正須要的數據,雖然你對它很親切。

不要被"hello"迷惑了,最初我看到它時很難把它和bin聯繫到一塊兒。"hello"是你的編輯器對hex2bin後的二進制流進行處理時,把二進制流當作文本處理(它們是文本編輯器,天然會把數據做爲文本處理),剛好能對應上可打印字符的ascii碼,就顯示了"hello",因此說文本和二進制文件底層都是bit流,看怎麼處理了。

bin2hex 解包

bin2hex(binString)則是將待處理的數據的二進制bit串進行16進制轉換,並返回相應的16進制形式的字符串,這裏的bin是說會將其做爲二進制流,轉換成對應的十六進制流,而後再以對應的字符串方式返回。好比"h"二進制bit串01101000,對應的十六進制是 0x68,相應的字符串形式是"68",依次繼續解包處理 "e" "l" "l" "o" 後獲得的字符串就是"68656c6c6f"

因此咱們若是想傳輸字符集在'0-9a-f'內的數據,可使用 hex2bin打包數據至二進制流節省空間,而後傳輸完成後再經過bin2hex解包得到源數據。

hex2bin(hexString)/bin2hex(binString) 對應 pack("H*", hexString)/unpack("H*", binString)

pack/unpack

只要提供了socket編程的語言,確定也同時提供了 pack/unpackpack/unpack的主要做用是方便咱們將數據打包至二進制流,而後以二進制流的方式解包。

H 十六進制

將十六進制的字符串打包至二進制流字符串,這裏有個很微妙的理解,好比字符串"68" 2bytes,若是做爲十六進制則其對應的二進制是00110110,對應的ascii文本字符是"h" 1byte,傳輸後咱們對h進行解包轉爲十六進制獲得"68",這種打包-傳輸-解包的方式節省了一半的傳輸量。

好比咱們有一串字符串數據"68656c6c6f",若是直接傳輸的話須要strlen("68656c6c6f") = 10 byte,你會發現這串數據徹底符合十六進制模式,若是進行以下處理咱們能夠節省了一半的傳輸數據量。

<?php
// 符合十六進制的字符串 0-9a-f
$data = "68656c6c6f";
echo "src data len:" . strlen($data) . PHP_EOL;

// 發送端 將 16進制 的字符串打包至 2進制
$package = pack("H*", $data);

//若是你輸出 $package 你會發現它是 "hello"
echo "packed data len:" . strlen($package) . PHP_EOL;

// 傳輸的數據量爲 strlen($package) 個字節

// 接收端解包
$data = unpack("H*", $package);
var_dump($data);

以上功能能夠用 hex2bin/bin2hex實現

<?php
// 符合十六進制的字符串 0-9a-f
$data = "68656c6c6f";

foreach (str_split($data) as $char) {
    echo sprintf("%08d", decbin(ord($char))) . ' ';
}
echo "data len: " . strlen($data) * 8 . ' bits' . PHP_EOL;

$package = hex2bin($data);
foreach (str_split($data) as $char) {
    echo sprintf("%08d", decbin(ord($char))) . ' ';
}
echo "package len: " . strlen($package) * 8 . ' bits' . PHP_EOL;

$data = bin2hex($package);
var_dump($data);

其實你會發現做爲打包傳輸的數據是hello,這裏可能有些反客爲主的慣性思惟,由於大部分時間對咱們有意義的數據是hello而不是 68656c6c6f,但在這裏68656c6c6f纔是咱們須要的數據,打包後獲得的hello反而是壓縮傳輸的中間數據。

n 無符號16位整形大端序 / N 無符號32位整形大端序

n是無符號短整型,固定2bytes,能夠將0 ~ 65536內的數值用固定2bytes表示,好比 "65536"用字符串表示須要5bytes,以 n 模式打包至二進制後只須要 2bytes,節省了傳輸空間。

N是無符號整形,固定4bytes,能夠將0~4294967296內的數值用固定4bytes表示。

固然,可能你的數值字符串是"0~99"的時候節省空間的優勢並不明顯,但另外一個優勢一直存在,那就是長度固定,在一些協議傳輸時dataLen . data的包結構是很須要dataLen是固定字節的數據。

好比

// package struct
00000000 00000001 00110110
|---dataLen 1---| |data h|

咱們取數據包 package 的前2bytes解包後獲得的數值就是數據包消息體的長度,如上所示長度爲 1,後面的data即爲1bytes 'h'

tcp流式數據時是很必須的,由於若是沒有包長度聲明,發送端連續發送多條消息,可能會致使粘包,接收端沒辦法正確的拆分消息:msg1msg2msg3 可能會被拆分紅 "msg" "1msg2ms" "g3"

而聲明瞭包長度後,咱們能夠先讀取固定長度的字節獲取到消息體長度,再讀取相應長度的消息體內容。

<?php
$foo = "hello world";
$bar = "i am sqrt_cat";

$package = "";
// 使用 n 打包 固定2bytes
$fooLenn = pack("n", strlen($foo));
$package = $fooLenn . $foo;

$barLenn = pack("n", strlen($bar));
$package .= $barLenn . $bar;

// 傳輸 $package "fooLen . foo . barLen . bar"

// 取前 2bytes 按 n 解包
$fooLen = unpack("n", substr($package, 0, 2))[1];
// 使用包消息體長度定義讀取消息體
// 從第 3byte 開始讀 前 2bytes表示長度
$foo = substr($package, 2, $fooLen);
echo $foo . PHP_EOL;

// 0 ~ (2 + fooLen) - 1 字節序爲 fooLen . foo
// (2 + fooLen) ~ (2 + fooLen) + 2 - 1 爲 barLen
$barLen = unpack("n", substr($package, (2 + $fooLen), 2))[1];
$bar = substr($package, (2 + $fooLen) + 2, $barLen);
echo $bar . PHP_EOL;

這樣就能夠靈活的解析流數據,在一些狀況下還節省了空間。

其餘模式你們能夠參考 http://www.javashuo.com/article/p-cyrqapbz-he.html

相關文章
相關標籤/搜索