php序列化(serialize)格式详解
1.前⾔
PHP (从 PHP 3.05 开始)为保存对象提供了⼀组序列化和反序列化的函数:serialize、unserialize。不过在 PHP ⼿册中对这两个函数的说明仅限于如何使⽤,⽽对序列化结果的格式却没做任何说明。因此,这对在其他语⾔中实现 PHP ⽅式的序列化来说,就⽐较⿇烦了。虽然以前也搜集了⼀些其他语⾔实现的 PHP 序列化的程序,不过这些实现都不完全,当序列化或反序列化⼀些⽐较复杂的对象时,就会出错了。于是我决定写⼀份关于 PHP 序列化格式详解的⽂档(也就是这⼀篇⽂档),以便在编写其他语⾔实现的 php 序列化程序时能有⼀个⽐较完整的参考。这篇⽂章中所写的内容是我通过编写程序测试和阅读 PHP 源代码得到的,所以,我不能 100% 保证所有的内容都是正确的,不过我会尽量保证我所写下的内容的正确性,对于我还不太清楚的地⽅,我会在⽂中明确指出,也希望⼤家能够给予补充和完善。2.概述
PHP 序列化后的内容是简单的⽂本格式,但是对字母⼤⼩写和空⽩(空格、回车、换⾏等)敏感,⽽且字符串是按照字节(或者说是 8 位的字符)计算的,因此,更合适的说法是 PHP 序列化后的内容是字节流格式。因此⽤其他语⾔实现时,如果所实现的语⾔中的字符串不是字节储存格式,⽽是 Unicode 储存格式的话,序列化后的内容不适合保存为字符串,⽽应保存为字节流对象或者字节数组,否则在与 PHP 进⾏数据交换时会产⽣错误。
PHP 对不同类型的数据⽤不同的字母进⾏标⽰,Yahoo 开发⽹站提供的 Using Serialized PHP with Yahoo! Web Services ⼀⽂中给出所有的字母标⽰及其含义:
a - array
b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
N 表⽰的是 NULL,⽽ b、d、i、s 表⽰的是四种标量类型,⽬前其它语⾔所实现的 PHP 序列化程序基本上都实现了对这些类型的序列化和反序列化,不过有⼀些实现中对 s (字符串)的实现存在问题。
a、O 属于最常⽤的复合类型,⼤部分其他语⾔的实现都很好的实现了对 a 的序列化和反序列化,但对 O 只实现了 PHP4 中对象序列化格式,⽽没有提供对 PHP 5 中扩展的对象序列化格式的⽀持。
r、R 分别表⽰对象引⽤和指针引⽤,这两个也⽐较有⽤,在序列化⽐较复杂的数组和对象时就会产⽣带有这两个标⽰的数据,后⾯我们将详细讲解这两个标⽰,⽬前这两个标⽰尚没有发现有其他语⾔的实现。
C 是 PHP5 中引⼊的,它表⽰⾃定义的对象序列化⽅式,尽管这对于其它语⾔来说是没有必要实现的,因为很少会⽤到它,但是后⾯还是会对它进⾏详细讲解的。
U 是 PHP6 中才引⼊的,它表⽰ Unicode 编码的字符串。因为 PHP6 中提供了 Unicode ⽅式保存字符串的能⼒,因此它提供了这种序列化字符串的格式,不过这个类型 PHP5、PHP4 都不⽀持,⽽这两个版本⽬前是主流,因此在其它语⾔实现该类型时,不推荐⽤它来进⾏序列化,不过可以实现它的反序列化过程。在后⾯我也会对它的格式进⾏说明。
最后还有⼀个 o,这也是我唯⼀还没弄清楚的⼀个数据类型标⽰。这个标⽰在 PHP3 中被引⼊⽤来序列化对象,但是到了 PHP4 以后就被 O 取代了。在 PHP3 的源代码中可以看到对 o 的序列化和反序列化与数组 a 基本上是⼀样的。但是在 PHP4、PHP5 和 PHP6 的源代码中序列化部分⾥都不到它的影⼦,但是在这⼏个版本的反序列化程序源代码中却都有对它的处理,不过把它处理成什么我还没弄清楚。因此对它暂时不再作更多说明了。
3.NULL 和标量类型的序列化
NULL 和标量类型的序列化是最简单的,也是构成符合类型序列化的基础。这部分内容相信许多 PHP 开发者都已经熟知。如果您感觉已经掌握了这部分内容,可以直接跳过这⼀章。
3.1.NULL 的序列化
在 PHP 中,NULL 被序列化为:
N; 3.2.boolean 型数据的序列化
boolean 型数据被序列化为:
b:<digit>;
其中 <digit> 为 0 或 1,当 boolean 型数据为 false 时,<digit> 为 0,否则为 1。
3.3.integer 型数据的序列化
integer 型数据(整数)被序列化为:
i:<number>;
其中 <number> 为⼀个整型数,范围为:-2147483648 到 2147483647。数字前可以有正负号,如果被序列化的数字超过这个范围,则会被序列化为浮点数类型⽽不是整型。如果序列化后的数字超过这个范围(PHP 本⾝序列化时不会发⽣这个问题),则反序列化时,将不会返回期望的数值。
3.4.double 型数据的序列化
double 型数据(浮点数)被序列化为:
d:<number>;
其中 <number> 为⼀个浮点数,其范围与 PHP 中浮点数的范围⼀样。可以表⽰成整数形式、浮点数形式和科学技术法形式。如果序列化⽆穷⼤数,则 <number> 为 INF,如果序列化负⽆穷⼤,则 <number> 为 -INF。序列化后的数字范围超过 PHP 能表⽰的最⼤值,则反序列化时返回⽆穷⼤(INF),如果序列化后的数字范围超过 PHP 所能表⽰的最⼩精度,则反序列化时返回 0。
3.5.string 型数据的序列化
string 型数据(字符串)被序列化为:
s:<length>:"<value>";
其中 <length> 是 <value> 的长度,<length> 是⾮负整数,数字前可以带有正号(+)。<value> 为字符串值,这⾥的每个字符都是单字节字符,其范围与 ASCII 码的 0 - 255 的字符相对应。每个字符都表⽰原字符含义,没有转义字符,<value> 两边的引号("")是必须的,但不计算在 <length> 当中。这⾥的 <value> 相当于⼀个字节流,⽽ <length> 是这个字节流的字节个数。
4.简单复合类型的序列化
PHP 中的复合类型有数组(array)和对象(object)两种,本章主要介绍在简单情况下这两种类型数据的序列化格式。关于嵌套定义的复合类型和⾃定义序列化⽅式的对象的序列化格式将在后⾯的章节详细讨论。
4.1.数组的序列化
数组(array)通常被序列化为:
a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}
其中 <n> 表⽰数组元素的个数,<key 1>、<key 2>……<key n> 表⽰数组下标,<value 1>、<value 2>……<value n> 表⽰与下标相对应的
数组元素的值。
下标的类型只能是整型或者字符串型,序列化后的格式跟整型和字符串型数据序列化后的格式相同。
数组元素值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。
4.2.对象的序列化
对象(object)通常被序列化为:
O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}
其中 <length> 表⽰对象的类名 <class name> 的字符串长度。<n> 表⽰对象中的字段1个数。这些字段包括在对象所在类及其祖先类中⽤var、public、protected 和 private 声明的字段,但是不包括 static 和 const 声明的静态字段。也就是说只有实例(instance)字段。
<filed name 1>、<filed name 2>……<filed name n>表⽰每个字段的字段名,⽽ <filed value 1>、<filed value 2>……<filed value n> 则表⽰与字段名所对应的字段值。
字段名是字符串型,序列化后格式与字符串型数据序列化后的格式相同。
字段值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。
但字段名的序列化与它们声明的可见性是有关的,下⾯重点讨论⼀下关于字段名的序列化。
4.3.对象字段名的序列化
var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进⾏序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $。
protected 声明的字段为保护字段,在所声明的类和该类的⼦类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前⾯会加上
\0*\0
的前缀。这⾥的 \0 表⽰ ASCII 码为 0 的字符,⽽不是 \0 组合。
private 声明的字段为私有字段,只在所声明的类中可见,在该类的⼦类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前⾯会加上
\0<declared class name>\0
的前缀。这⾥ <declared class name> 表⽰的是声明该私有字段的类的类名,⽽不是被序列化的对象的类名。因为声明该私有字段的类不⼀定是被序列化的对象的类,⽽有可能是它的祖先类。
字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。
1注:在 PHP ⼿册中,字段被称为属性,⽽实际上,在 PHP 5 中引⼊的⽤ __set、__get 来定义的对象成员更适合叫做属性。因为⽤
__set、__get 来定义的对象成员与其它语⾔中的属性的⾏为是⼀致,⽽ PHP ⼿册中所说的属性实际上在其他语⾔中(例如:C#)中被称为字段,为了避免混淆,这⾥也称为字段,⽽不是属性。
5.嵌套复合类型的序列化
上⼀章讨论了简单的复合类型的序列化,⼤家会发现对于简单的数组和对象其实也很容易。但是如果遇到⾃⼰包含⾃⼰或者 A 包含 B,B ⼜包含 A 这类的对象或数组时,PHP ⼜该如何序列化这种对象和数组呢?本章我们就来讨论这种情况下的序列化形式。
5.1.对象引⽤和指针引⽤
在 PHP 中,标量类型数据是值传递的,⽽复合类型数据(对象和数组)是引⽤传递的。但是复合类型数据的引⽤传递和⽤ & 符号明确指定的引⽤传递是有区别的,前者的引⽤传递是对象引⽤,⽽后者是指针引⽤。
在解释对象引⽤和指针引⽤之前,先让我们看⼏个例⼦。
<?php
echo "<pre>";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;php实例代码详解
$b = new SampleClass();
$b->value = &$b;
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
echo "</pre>";
?>
这个例⼦的输出结果是这样的:
O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}
⼤家会发现,这⾥变量 $a 的 value 字段的值被序列化成了 r:1,⽽ $b 的 value 字段的值被序列化成了 R:1。
但是对象引⽤和指针引⽤到底有什么区别呢?
⼤家可以看下⾯这个例⼦:
echo "<pre>";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
echo "</pre>";
⼤家会发现,运⾏结果也许出乎你的预料:
object(SampleClass)#1 (1) {
["value"]=>
int(1)
}
int(1)
改变 $a->value 的值仅仅是改变了 $a->value 的值,⽽改变 $b->value 的值却改变了 $b 本⾝,这就是对象引⽤和指针引⽤的区别。
不过很不幸的是,PHP 对数组的序列化犯了⼀个错误,虽然数组本⾝在传递时也是对象引⽤传递,但是在序列化时,PHP 似乎忘记了这⼀点,看下⾯的例⼦:
echo "<pre>";
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo $a["value"]["value"][1];
echo "\n";
$a = unserialize(serialize($a));
echo $a["value"]["value"][1];
echo "</pre>";
结果是:
1
⼤家会发现,将原数组序列化再反序列化后,数组结构变了。原本 $a["value"]["value"][1] 中的值 1,在反序列化之后丢失了。
原因是什么呢?让我们输出序列化之后的结果来看⼀看:
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo serialize($a);
结果是:
a:2:{i:1;i:1;s:5:"value";a:2:{i:1;i:1;s:5:"value";N;}}
原来,序列化之后,$a["value"]["value"] 变成了 NULL,⽽不是⼀个对象引⽤。
也就是说,PHP 只对对象在序列化时才会⽣成对象引⽤标⽰(r)。对所有的标量类型和数组(也包括 NULL)序列化时都不会⽣成对象引⽤。但是如果明确使⽤了 & 符号作的引⽤,在序列化时,会被序列化为指针引⽤标⽰(R)。
5.2.引⽤标⽰后的数字
在上⾯的例⼦中⼤家可能已经看到了,对象引⽤(r)和指针引⽤(R)的格式为:
r:<number>;
R:<number>;
⼤家⼀定很奇怪后⾯这个 <number> 是什么吧?本节我们就来详细讨论这个问题。
这个 <number> 简单的说,就是所引⽤的对象在序列化串中第⼀次出现的位置,但是这个位置不是指字符的位置,⽽是指对象(这⾥的对象是泛指所有类型的量,⽽不仅限于对象类型)的位置。
我想⼤家可能还不是很明⽩,那么我来举例说明⼀下:
class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}
$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;
echo serialize($a);
这个例⼦的结果是:
O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}
在这个例⼦中,⾸先序列化的对象是 ClassA 的⼀个对象,那么给它编号为 1,接下来要序列化的是这个对象的⼏个成员,第⼀个被序列化
的成员是 int 字段,那它的编号就为 2,接下来被序列化的成员是 str,那它的编号就是 3,依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; ,在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的⼀个引⽤,⽽ str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了。
PHP 是如何来编号被序列化的对象的呢?实际上,PHP 在序列化时,⾸先建⽴⼀个空表,然后每个被序列化的对象在被序列化之前,都需要先计算该对象的 Hash 值,然后判断该 Hash 值是否已经出现在该表中了,如果没有出现,就把该 Hash 值添加到这个表的最后,返回添加成功。如果出现了,则返回添加失败,但是在返回失败前先判断该对象是否是⼀个引⽤(⽤ & 符号定义的引⽤),如果不是则也把 Hash 值添加到表后(尽管返回的是添加失败)。如果返回失败,则同时返回上⼀次出现的位置。
在添加 Hash 值到表中之后,如果添加失败,则判断添加的是⼀个引⽤还是⼀个对象,如果是引⽤,则返回 R 标⽰,如果是对象,则返回 r 标⽰。因为失败时,会同时返回上⼀次出现的位置,因此,R 和 r 标⽰后⾯的数字,就是这个位置。
5.3.对象引⽤的反序列化
PHP 在反序列化处理对象引⽤时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本⾝⽣成的,⽽是⼈为构造或者⽤其它语⾔⽣成的,即使对象引⽤指向的不是⼀个对象,它也能正确地按照对象
引⽤所指向的数据进⾏反序列化。例如:
echo "<pre>";
class StrClass {
var $a;
var $b;
}
$a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}');
var_dump($a);
echo "</pre>";
运⾏结果:
object(StrClass)#1 (2) {
["a"]=>
string(5) "Hello"
["b"]=>
string(5) "Hello"
}
⼤家会发现,上⾯的例⼦反序列化后,$a->b 的值与 $a->a 的值是⼀样的,尽管 $a->a 不是⼀个对象,⽽是⼀个字符串。因此如果⼤家⽤其它语⾔来实现序列化的话,不⼀定⾮要把 string 作为标量类型来处理,即使按照对象引⽤来序列化拥有相同字符串内容的复合类型,⽤PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占⽤的空间。
6.⾃定义对象序列化 6.1.PHP 4 中⾃定义对象序列化
PHP 4 中提供了 __sleep 和 __wakeup 这两个⽅法来⾃定义对象的序列化。不过这两个函数并不改变对象序列化的格式,影响的仅仅是被序列化字段的个数。关于它们的介绍,在 PHP ⼿册中写的还算⽐较详细。这⾥就不再多做介绍了。
6.2.PHP 5 中⾃定义对象序列化
PHP 5 中增加了接⼝(interface)功能。PHP 5 本⾝提供了⼀个 Serializable 接⼝,如果⽤户在⾃⼰定义的类中实现了这个接⼝,那么在该类的对象序列化时,就会被按照⽤户实现的⽅式去进⾏序列化,并且序列化后的标⽰不再是 O,⽽改为 C。C 标⽰的格式如下:
C:<name length>:"<class name>":<data length>:{<data>}
其中 <name length> 表⽰类名 <class name> 的长度,<data length> 表⽰⾃定义序列化数据 <data> 的长度,⽽⾃定义的序列化数据 <data>是完全的⽤户⾃⼰定义的格式,与 PHP 序列化格式可以完全⽆关,这部分数据由⽤户⾃⼰实现的序列化和反序列化接⼝⽅法来管理。Serializable 接⼝中定义了 2 个⽅法,serialize() 和 unserialize($data),这两个⽅法不会被直接调⽤,⽽是在调⽤ PHP 序列化函数时,被⾃动调⽤。其中 serialize 函数没有参数,它的返回值就是 <data> 的内容。⽽ unserialize($data) 有⼀个参数 $data,这个参数的值就是
<data> 的内容。这样⼤家应该就明⽩了,实际上接⼝中 serialize ⽅法就是让⽤户来⾃⼰序列化对象中的内容,序列化后的内容格式,PHP 并不关⼼,PHP 只负责把它充填到 <data> 中,等到反序列化时,PHP 只负责取出这部分内容,然后传给⽤户实现的 unserialize($data) 接⼝⽅法,让⽤户⾃⼰去反序列化这部分内容。
下⾯举个简单的例⼦,来说明 Serializable 接⼝的使⽤:
class MyClass implements Serializable
{
public $member;
function MyClass()
{
$this->member = 'member value';
}
public function serialize()
{
return wddx_serialize_value($this->member);
}
public function unserialize($data)
{
$this->member = wddx_deserialize($data);
}
}
$a = new MyClass();
echo serialize($a);
echo "\n";
print_r(unserialize(serialize($a)));
输出结果为(浏览器中的源代码):
C:7:"MyClass":90:{<wddxPacket version='1.0'><header/><data><string>member value</string></data></wddxPacket>}
MyClass Object
(
[member] => member value
)
因此如果想⽤其它语⾔来实现 PHP 序列化中的 C 标⽰的话,也需要提供⼀种这样的机制,让⽤户⾃定义类时,能够⾃⼰在反序列化时处理<data> 内容,否则,这些内容就⽆法被反序列化了。
7.Unicode 字符串的序列化
好了,最后再谈谈 PHP 6 中关于 Unicode 字符串序列化的问题吧。
说实话,我不怎么喜欢把字符串搞成双字节 Unicode 这种编码的东西。JavaScript 中也是⽤这样的字符串,因此在处理字节流的东西时,反⽽⾮常的不⽅便。C# 虽然也是⽤这种⽅式来编码字符串,不过还
好的是,它提供了全⾯的编码转换机制,⽽且提供这种字符串到字节流(实际上是到字节数组)的转换,所以处理起来还算是可以。但是对于不熟悉这个的⼈来说,转来转去就是个⿇烦。
PHP 6 之前⼀直是按字节来编码字符串的,到了 PHP 6 突然冒出个 Unicode 编码的字符串来,虽然是可选的,但仍然让⼈觉得⾮常不舒服,如果配置不当,⽼的程序兼容性都成问题。
当然加了这个东西以后,许多⽼的与字符串有关的函数都进⾏了修改。序列化函数也不例外。因此,PHP 6 中增加了专门的 Unicode 字符串序列化标⽰ U。PHP 6 中对 Unicode 字符串的序列化格式如下:
U:<length>:"<unicode string>";
这⾥ <length> 是指原 Unicode String 的长度,⽽不是 <unicode string> 的长度,因为 <unicode string> 是经过编码以后的字节流了。
但是还有⼀点要注意,<length> 尽管是原 Unicode String 的长度,但是也不是只它的字节数,当然也不完全是指它的字符数,确切的说是之它的字符单位数。因为 Unicode String 中采⽤的是 UTF16 编码,这种编码⽅式使⽤ 16 位来表⽰⼀个字符的,但是并不是所有的都是可以⽤ 16 位表⽰的,因此有些字符需要两个 16 位来表⽰⼀个字符。因此,在 UTF16 编码中,16 位字符算作⼀个字符单位,⼀个实际的
字符可能就是⼀个字符单位,也有可能由两个字符单位组成。因此, Unicode String 中字符数并不总是等于字符单位数,⽽这⾥的 <length> 指的就是字符单位数,⽽不是字符数。
那 <unicode string> ⼜是怎样被编码的呢?实际上,它的编码也很简单,对于编码⼩于 128 的字符(但不包括 \),按照单个字节写⼊,对于⼤于 128 的字符和 \ 字符,则转化为 16 进制编码的字符串,以 \ 作为开头,后⾯四个字节分别是这个字符单位的 16 进制编码,顺序按照由⾼位到低位排列,也就是第 16-13 位所对应的16进制数字字符(abcdef 这⼏个字母是⼩写)作为第⼀个字节,第 12-9 位作为第⼆个字节,第 8-5 位作为第三个字节,最后的第 4-1 位作为第四个字节。依次编码下来,得到的就是 <uncode string> 的内容了。
我认为对于其他语⾔来说,没有必要实现这种序列化⽅式,因为⽤这种⽅式序列化的内容,对于⽬前的主流 PHP 服务器来说都是不⽀持的,不过倒是可以实现它的反序列化,这样将来即使跟 PHP 6 进⾏数据交换,也可以互相读懂了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。