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

栏目: PHP · 发布时间: 4年前

内容简介:计算机在存储或传输数据时都是以 bit 流的形式(二进制),文本文件和二进制文件的主要区别就是在于文本文件是有字符集的,例如存储

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

计算机在存储或传输数据时都是以 bit 流的形式(二进制),文本文件和二进制文件的主要区别就是在于文本文件是有字符集的, ascii/utf8/utf16 等,读取时会将二进制流解码成对应的字符集字符。而二进制文件则简单的将数据作为二进制流处理,使用文本编辑器打开时, ascii 解码, 1byte 1byte 的处理并,有的落在 ascii 可打印字符中的就显示,没有落在其中的就是我们看到的乱码了。

例如

存储

ascii 文本数据 hello 存储的二进制流如下
存储的是每个字符的 ASCII 码
01101000 01100101 01101100 01101100 01101111

读取

按 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 位代表的什么。

因为 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 时,我们得到的是 “我”。

如果我们将其作为二进制文件处理的话得到的结果是 int(15108241) 数值。

<?php
// 将二进制字符串转为对应的十进制
echo bindec('111001101000100010010001');
//result
int(15108241)

hex2bin / bin2hex

其实这俩货让我迷惑了很久,很长一段时间我都不明白他俩到底有什么用,输出结果不难理解,还被同事的代码带过节奏,因为我看到他经常在发送数据前通过 bin2hex 转化一下再发送,我惯性的认为难不成可以压缩数据量?

而这一次,我很刻意的将 hex2bin 写在了 bin2hex 的前面。

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;

这样就可以灵活的解析流数据,在一些情况下还节省了空间。

其他模式大家可以参考 https://segmentfault.com/a/11...


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java核心技术及面试指南

Java核心技术及面试指南

金华、胡书敏、周国华、吴倍敏 / 北京大学出版社 / 2018-9-1 / 59.00

本书根据大多数软件公司对高级开发的普遍标准,为在Java 方面零基础和开发经验在3 年以下的初级程序员提供了升级到高级工程师的路径,并以项目开发和面试为导向,精准地讲述升级必备的技能要点。具体来讲,本书围绕项目常用技术点,重新梳理了基本语法点、面向对象思想、集合对象、异常处理、数据库操作、JDBC、IO 操作、反射和多线程等知识点。 此外,本书还提到了对项目开发很有帮助的“设计模式”和“虚拟......一起来看看 《Java核心技术及面试指南》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具