Submitted by mpeg on 2008, July 30, 10:37 AM. Php
任何一款拥有socket操作能力的语言都有一个专门用于组包的函数,php也不例外!
用了很久php了却很少有机会用php进行一些二进制操作。 最近用php写一个socket客户端连接一个用C++语言开发的游戏服务端。 服务器端开发人员使用了二进制的形式来定义协议的格式。协议格式如下:
包头(2bytes)+加密(1byte)+命令码(2bytes)+帧内容
1.包头的内容是记录帧内容的长度;
2. 加密:0表示不加密,1表示加密;
3. 命令码为服务端命令识别符号;
一开始不了解php原来有pack可以来组装二进制包, 走了弯路,让服务端开发人员用C语言帮忙开发了的几个内存操作函数,按照协议规则返回二进制包,然后我将这几个方法编译成一组扩展函数供php使用。
解释:
pack TEMPLATE, LIST
这个函数接收一个普通 Perl 数值的 LIST 并且根据 TEMPLATE 把它们转换成一个字节串并且返回该字串。参数列表在必要时会被填充或者截除。也就是说,如果你提供的参数比 TEMPLATE 要求的少,pack 假设缺的都是空值。如果你提供的参数比 TEMPLATE 要求的多,那么多余的参数被忽略。在 TEMPLATE 里无法识别的格式元素将会抛出一个例外。
这个模板把该字串的结构描述成一个域序列。每个域都由一个字符代表,描述该值的类型和其编码。比如,一个格式字符 N 声明一个四个字节的无符号整数,字节序是大头在前。
域是以模板中给出的顺序包装的。比如,如果要把一个一字节的无符号整数和一个单精度浮点数值包装到一个字串里,你要说:
$string = pack("CF", 244, 3.14);
返回的字串的第一个字节的值是 244。剩下的字节是 3.14 作为单精度浮点数的编码。浮点数的具体编码方式取决于你的计算机的硬件。
有些包装时要考虑的重要事情是:
数据的类型(比如是整数还是浮点还是字串),
数值的范围(比如你的整数是否放在一个,两个,四个,或者甚至八个字节里;或者你包装的是一个 8 位字符还是 Unicode 字符。),
你的整数是有符号还是无符号,以及
使用的编码(比如说本机,包装位和字节时小头在前,或者是大头在前)。
话归正题,本文是介绍如何使用pack和unpack这两个方法的。php官方手册举例太少,不能很容易理解,特别是那些格式化参数的使用。
- a 一个填充空的字节串
- A 一个填充空格的字节串
- b 一个位串,在每个字节里位的顺序都是升序
- B 一个位串,在每个字节里位的顺序都是降序
- c 一个有符号 char(8位整数)值
- C 一个无符号 char(8位整数)值;关于 Unicode 参阅 U
- d 本机格式的双精度浮点数
- f 本机格式的单精度浮点数
- h 一个十六进制串,低四位在前
- H 一个十六进制串,高四位在前
- i 一个有符号整数值,本机格式
- I 一个无符号整数值,本机格式
- l 一个有符号长整形,总是 32 位
- L 一个无符号长整形,总是 32 位
- n 一个 16位短整形,“网络”字节序(大头在前)
- N 一个 32 位短整形,“网络”字节序(大头在前)
- p 一个指向空结尾的字串的指针
- P 一个指向定长字串的指针
- q 一个有符号四倍(64位整数)值
- Q 一个无符号四倍(64位整数)值
- s 一个有符号短整数值,总是 16 位
- S 一个无符号短整数值,总是 16 位,字节序跟机器芯片有关
- u 一个无编码的字串
- U 一个 Unicode 字符数字
- v 一个“VAX”字节序(小头在前)的 16 位短整数
- V 一个“VAX”字节序(小头在前)的 32 位短整数
- w 一个 BER 压缩的整数
- x 一个空字节(向前忽略一个字节)
- X 备份一个字节
- Z 一个空结束的(和空填充的)字节串
- @ 用空字节填充绝对位置
你可以在你的 TEMPLATE 里自由地使用空白和注释。注释以惯用的 # 符号开头并且延伸到 TEMPLATE 里第一个换行符(如果存在)。
每个字母后面都可以跟着一个数字,表示 count(计数),解释成某种形式的重复计数或者长度,具体情况取决于格式。除了a,A,b,B,h,H,P,和 Z 之外,所有格式的 count 都是重复次数,因此 pack 从 LIST 里吃进那么多数值。如果 count 是一个 * 表示剩下的所有东西。
a,A 和 Z 格式只吃进一个数值,但是把它当作一个长度为 count 的字节串打包,并根据需要填充空或者空格。在解包的时候,A 抽去结尾的空格和空,Z 抽去第一个空后面的所有东西,而 a 原封不动地返回文本数据。打包的时候,a 和 Z 是相同的。
与之类似,b 和 B 格式打包一个长度为 count 的位串。输入域里的每个字节都以每个输入字节的最低位(也就是 ord($byte) % 2)为基础生成结果中的 1 个位。方便一点来说,这意味着字节 0 和 1 生成位 0 和 1。从输入字串的开头开始,每 8 个字节转换成一个字节的输出。如果输入字串的长度不能被 8 整除,那么余下的部分用 0 补齐。类似,在 uppack 的时候,任何额外的位都被忽略。如果输入字串比需要的长,那么多出来的部分被忽略。count 如果是 * 意思是使用输入域的所有字节。在解包的时候,这些位转换成一个 0 和 1 组成的字串。
h 和 H 格式打包一个由 count 个半字节(4 位组,常用于代表十六进制位。)组成的字串。
p 格式打包一个指向一个空结尾的字串。你有责任确保该字串不是一个临时数值(因为临时值可能在你能使用打包的结果之前很可能被释放掉)。P 格式打包一个指向一个结构的指针,该结构的大小由 count 指明。如果对应的 p 或 P 的值是 undef,则创建一个空指针。
/ 字符允许对这样一个字串进行打包或解包:这个打了包的结构包含一个字节数以及后面跟着字串本身。你可以这样写 length-item/string-item。length-item 可以是任意 pack 模板字符,并且描述长度值是如何打包的。最常用的可能是那些整数打包的东西,比如 n(用于打包 Java 字串),w(用于打包 ASN.1 或 SNMP)以及 N(用于 Sun XDR)。string-item 目前必须是 A*,a*,或者 Z*。对于 unpack 而言,字串的长度是从 length-item 里获取的,但是如果你放 * 进去,那么它将被忽略。
- unpack 'C/a', "/04Gurusamy"; # 生成 'Guru'
- uppack 'a3/A* A*', '077 Bond J '; # 生成 (' Bond', 'J')
- pack 'n/a* w/a*', 'hell', ','world'; # 生成 "hello, world"
length-item 不会从 unpack 明确地返回。向 length-item 字母加一个 count 不一定能干有用的事,除非该字母是 A,a,或 Z。用带 length-item 的 a 或 Z 进行打包可能会引入空(/0)字符,这时候,Perl 在会认为它不是合法数字字串。
整数格式 s,S,l,和 L 的后面可以紧跟一个 !,表示本机的短整数或者长整数,而不是各自准确的 16 位和 32 位。现在,这是一个在许多 64 位平台上的问题,在这些平台上本地 C 编译器看到本机短整数和长整数可能和上面的这些值不同。(i! 和 I! 也可以用,不过只不过是为了保持完整;它们与 i 和 I 相同。)
你可以通过 Config 模块获取制作你的 Perl 的平台上的本机的 short,int,long 和 long long 的实际长度:
- use Config;
- print $Config{shortsize}, "/n";
- print $Config{intsize}, "/n";
- print $Config{longsize}, "/n";
- print $Config{longlongsize}, "/n";
这里只是说 Configure 知道一个 long long 的大小,但着并不意味着你就能用 q 和 Q。(有些系统能有,不过你用的系统很可能还没有。)
长度大于一个字节的整数格式(s,S,i,I,l,和 L)都是天生不能在不同处理器之间移植的,因为它们要遵从本机字节序和位权重序的规则。如果你需要可移植的整数,那么使用格式 n,N, v,和 V;因为它们是字节权重序并且是尺寸已知的。
浮点数只以本机格式存在。因为浮点格式的千差万别而且缺乏一种标准的“网络”表现形式,所以没有做任何浮点数交换的工具。这意味着在一台机器上打包了的浮点数数据可能不能在另外一台上读。甚至如果两台机器都使用 IEEE 浮点数算法的话,这都仍然是一个问题,因为与权重相关的内存表现形式不是 IEEE 规范的一部分。
Perl 在内部使用双精度数进行所有浮点数计算,所以从 double 转换成 float,然后又转换成 float 会损失精度。这就意味着 unpack("f", pack("f", $foo)) 可能不等于 $foo。
你有责任为其他程序考虑任何对齐或填充的问题,尤其是那些带着 C 编译器自己的异质概念的 C struct 的程序,C 编译器在不同的体系结构上对 C struct 如何布局有着巨大的差别。你可能需要在打包时增加足够的 x 来弥补这个问题。比如:
- struct foo {
- unsigned char c;
- float f;
- };
可以写成一个“C x f”格式,一个“C x3 f”格式,或者甚至是“f C”格式——而且这只是其中一部分。pack 和 unpack 函数把它们的输入和输出当作平面的字节序列处理,因为它们不知道这些字节要去哪儿,或者从哪儿来。
PACK函数
string pack ( string $format [, mixed $args [, mixed $...]] )
一些规则:
1.每个字母后面都可以跟着一个数字,表示 count(计数),如果 count 是一个 * 表示剩下的所有东西。
2.如果你提供的参数比 $format 要求的少,pack 假设缺的都是空值。如果你提供的参数比 $format 要求的多,那么多余的参数被忽略。
下面还是用例子来说明用法会容易理解一点:
关于Pack:
- $out = pack("CCCC", 65, 66, 67, 68); # $out 等于"ABCD"
- $out = pack("C4", 65, 66, 67, 68); # 一样的东西
- $foo = pack("U4", 0x24b6, 0x24b7, 0x24b8, 0x24b9);
- $out = pack("CCxxCC", 65, 66, 67, 68); # $out 等于 "AB/0/0CD"
打包你的短整数并不意味着你就可移植了:
$out = pack("s2", 1, 2);
# 在小头在前的机器上是 "/1/0/2/0"
# 在大头在前的机器上是 "/0/1/0/2"
在二进制和十六进制包装上,count 指的是位或者半字节的数量,而不是生成的字节数量:
$out = pack("B32", "...");
$out = pack("H8", "5065726c"); # 都生成“Perl”
a 域里的长度只应用于一个字串:
$out = pack("a4", "abcd", "x", "y", "z"); # "abcd"
要绕开这个限制,使用多倍声明:
$out = pack("aaaa", "abcd", "x", "y", "z"); # "axyz"
$out = pack("a" x 4, "abcd", "x", "y", "z"); # "axyz"
a 格式做空填充:
$out = pack("a14", "abcdefg"); # " abcdefg/0/0/0/0/0/0"
关于unpack:
array unpack ( string $format, string $data )
$data = "010000020007";
unpack("Sint1/Cchar1/Sint2/Cchar2",$data);
## array('int1'=>1, 'char1'=>'0','int2'=>2,'char2'=>7);
- $lastact = pack('SCSa32a32',0x0040, 0x00, 0x0006, $username, $passwd );
- unpack('Sint1/Cchar1/Sint2/Cchar2/',$lastmessage