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

相关文章
相关标签/搜索