PHP的二进制封包(pack/unpack)

通过 TCP/IP 协议传输数据经常会用二进制数据包的形式,在 PHP 中可使用 pack() 和 unpack() 函数进行二进制封包和解包,通过 socket 建立 TCP 连接,并将数据包传输出去。

字节序

在不同的计算机体系结构中,对于数据(比特、字节、字)等的存储和传输机制有所不同,因而引发了计算机领域中一个潜在但是又很重要的问题,即通信双方交流的信息单元应该以什么样的顺序进行传送。如果达不成一致的规则,计算机的通信与存储将会无法进行。目前在各种体系的计算机中通常采用的字节存储机制主要有两种:大端(Big-endian)和小端(Little-endian)。这里所说的大端和小端即是字节序。网络字节序是指大端序。TCP/IP都是采用网络字节序的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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 "小端序";
}

//测试
//php -f pack.php
//小端序

Pack参数说明

CodeDescription
a将字符串空白以 NULL 字符填满
A将字符串空白以 SPACE 字符 (空格) 填满
h16进制字符串,低位在前以半字节为单位
H16进制字符串,高位在前以半字节为单位
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 字符到绝对位置

使用例子

比如现在要通过PHP发送数据包到服务器来登录。在仅需要提供用户名(最多30个字节)和密码(md5之后固定为32字节)的情况下,可以构造如下数据包(当然这事先需要跟服务器协商好数据包的规范,本例以网络字节序通信)

包结构

字段字节数说明
包头定长每一个通信消息必须包含的内容
包体不定长根据每个通信消息的不同产生变化

包头详细内容

字段字节数类型说明
pkg_len2ushort整个包的长度,不超过4K
version1uchar通讯协议版本号
command_id2ushort消息命令ID
result2short请求时不起作用;请求返回时使用

Pack打包

包头是定长的,通过计算可知包头占7个字节,并且包头在包体之前。比如用户test需要登录,密码是123456

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$version = 1; //协议版本
$result = 0;
$command_id = 1001; //消息ID
$username = "test"; //用户账号
$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);

以上的代码中,pack(“a30a32”, $username, $password);a30表示30个a,您当然可以连续写30个a,但我想您不会这么傻。如果是a*的话,则表示任意多个a。通过服务器端的输出来看,PHP发送了30个字节过去,服务器端也接收了30个字节,但因为填充的\0是空字符,所以您不会看到有什么不一样的地方,a32同理

unpack解包

unpack是用来解包经过pack打包的数据包,如果成功,则返回数组。其中格式化字符和执行pack时一一对应,但是需要额外的指定一个key,用作返回数组的key。多个字段用/分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$bin = @pack("a9SS", "test", 20, 1);
$data = @unpack("a9name/sage/Sgender", $bin);

if (is_array($data))
{
print_r($data);
}


//测试
$ php -f pack.php
Array
(
[name] => test
[age] => 20
[gender] => 1
)

参考文章

PHP: 深入pack/unpack
PHP: chr和pack、unpack那些事
PHP: pack/unpack补遗