文本文件 / 二进制文件 / 二进制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)
起初看到 bin
就以为是底层的二进制 bit
位,后来才明白是相应的 二进制bit位编码文本串
。
$php_int_size = PHP_INT_SIZE;
$php_int_max = PHP_INT_MAX;
echo "PHP INT 字节数(当前系统): " . $php_int_size . PHP_EOL;
echo "PHP最大整数十进制: " . $php_int_max . PHP_EOL . PHP_EOL;
echo "十进制数值的二进制字符串: " . ($php_int_max_bin_str = decbin($php_int_max)) . " " . strlen($php_int_max_bin_str) . PHP_EOL;
echo "二进制字符串的十进制数值: " . bindec($php_int_max_bin_str) . PHP_EOL . PHP_EOL;
echo "十进制数值的8进制字符串: " . ($php_int_max_oct_str = decoct($php_int_max)) . " " . strlen($php_int_max_oct_str) . PHP_EOL;
echo "8进制字符串的十进制数值: " . octdec($php_int_max_oct_str) . PHP_EOL . PHP_EOL;
echo "十进制数值的16进制字符串: " . ($php_int_max_hex_str = dechex($php_int_max)) . " " . strlen($php_int_max_hex_str) . PHP_EOL;
echo "16进制字符串的十进制数值: " . hexdec($php_int_max_hex_str) . PHP_EOL . PHP_EOL;
PHP INT 字节数(当前系统): 8
PHP最大整数十进制: 9223372036854775807
十进制数值的二进制字符串: 111111111111111111111111111111111111111111111111111111111111111 63
二进制字符串的十进制数值: 9223372036854775807
十进制数值的8进制字符串: 777777777777777777777 21
8进制字符串的十进制数值: 9223372036854775807
十进制数值的16进制字符串: 7fffffffffffffff 16
16进制字符串的十进制数值: 9223372036854775807
hex2bin / bin2hex
其实这俩货让我迷惑了很久,很长一段时间我都不明白他俩到底有什么用,输出结果不难理解,还被同事的代码带过节奏,因为我看到他经常在发送数据前通过 bin2hex
转化一下再发送,我惯性的认为难不成可以压缩数据量?
而这一次,我很刻意的将 hex2bin
写在了 bin2hex
的前面。
这里的 2bin
代表的是 二进制bit流
,而非前面所提的 bindec/decbin
中的 bin
所代表的 二进制bit流的字符串文本
。 00000001 1字节 数值 1
和 "00000001 8字节 字符串"
的区别。
不过还是要谨记开篇时讲的,文本
和 二进制
本质上都是 bit
流,当你用编辑器查看输出结果时,如果落在了有效的字符集中,那就按字符显示,否则就是二进制乱码。
hex2bin 打包
hex2bin(hexString)
的作用是将字符串作为 十六进制
的模式进行处理,如何处理,"68656c6c6f"
会被处理成 "68" "65" "6c" "6c" "6f"
,然后转换成对应的二进制数据(注意:是二进制数据,不是二进制数据对应的二进制字符串,decbin 方法提供这一功能,将数据转换成二进制对应的字符串
),"68"(注意是字符串 2bytes)
转为二进制数值是 01101000(注意是数值 1byte)
,输出至终端其实就是 h(1bytes)
的,依次处理后,我们成功的将 10bytes
的字符串 "68656c6c6f"
转换成了 5bytes
的字符串 "hello"
,只不过 "hello"
并不是我们真正需要的数据,虽然你对它很亲切。
不要被 "hello"
迷惑了,最初我看到它时很难把它和 bin
联系到一起。 "hello"
是你的编辑器对 hex2bin
后的 二进制流
进行处理时,把 二进制流
当做 文本
处理(它们是文本编辑器,自然会把数据作为文本处理),恰好能对应上可打印字符的 ascii码
,就显示了 "hello"
,所以说文本和二进制文件底层都是 bit流
,看怎么处理了。
数据使用 hex
字符串表征时,2字节
一个单位,分别代表二进制的高4位
和 低4位
,例如 0000 0001 1byte
hex
后为 “01” 2byte
。
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/unpack
,pack/unpack
的主要作用是方便我们在一些场景下将数据打包至 二进制流
,然后以 二进制流
的方式解包。
H 十六进制
将 十六进制
的字符串打包至 二进制流
,这里有个很微妙的理解,比如字符串 "68" 2bytes
,如果作为 十六进制
则其对应的 二进制
是 01101000
,对应的 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;
这样就可以灵活的解析流数据,在一些情况下还节省了空间。
其他模式大家可以参考: pack / unpack 参数详解
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。