PHP 序列化(serialize)格式詳解

 

1.前言

PHP (從 PHP 3.05 開始)爲保存對象提供了一組序列化和反序列化的函數:serialize、unserialize。不過在 PHP 手冊中對這兩個函數的說明僅限於如何使用,而對序列化結果的格式卻沒做任何說明。因此,這對在其他語言中實現 PHP 方式的序列化來說,就比較麻煩了。雖然以前也蒐集了一些其他語言實現的 PHP 序列化的程序,不過這些實現都不完全,當序列化或反序列化一些比較複雜的對象時,就會出錯了。於是我決定寫一份關於 PHP 序列化格式詳解的文檔(也就是這一篇文檔),以便在編寫其他語言實現的 php 序列化程序時能有一個比較完整的參考。這篇文章中所寫的內容是我通過編寫程序測試和閱讀 PHP 源代碼得到的,所以,我不能 100% 保證所有的內容都是正確的,不過我會盡量保證我所寫下的內容的正確性,對於我還不太清楚的地方,我會在文中明確指出,也希望大家能夠給予補充和完善。
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 的源代碼中序列化部分裏都找不到它的影子,但是在這幾個版本的反序列化程序源代碼中卻都有對它的處理,不過把它處理成什麼我還沒弄清楚。因此對它暫時不再作更多說明了。

補充

最近的 PHP CVS 版本中,序列化的方式有所變化,基本類型的序列化仍然保持原來的格式,只是對 Unicode 支持上,有了新的進展。另外,對普通字符串的序列化也分成了 2 種。一種是 non-escaped 字符串,也就是我們上面說的那個小寫 s 標識的字符串;另一種是 escaped 字符串,這種字符串格式用大寫 S 標識。所以上面那個表現在應該改爲:
  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - non-escaped binary string
  • S - escaped binary string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string

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。當浮點數爲非數時,被序列化爲 NAN,NAN 反序列化時返回 0。但其它語言可以將 NAN 反序列化爲相應語言所支持的 NaN 表示。

3.5.string 型數據的序列化

string 型數據(字符串)被序列化爲:
s:<length>:"<value>";
其中 <length> 是 <value> 的長度,<length> 是非負整數,數字前可以帶有正號(+)。<value> 爲字符串值,這裏的每個字符都是單字節字符,其範圍與 ASCII 碼的 0 - 255 的字符相對應。每個字符都表示原字符含義,沒有轉義字符,<value> 兩邊的引號("")是必須的,但不計算在 <length> 當中。這裏的 <value> 相當於一個字節流,而 <length> 是這個字節流的字節個數。

補充

在 PHP5 最新的 CVS 中(也就是將來要發佈的 PHP6),上面對於 string 型數據的序列化方式已經被下面這種所取代,但是 PHP6 仍然支持上面那種序列化方式的反序列化。
新的序列化方式叫做 escaped binary string 方式,這是相對與上面那種 non-escaped binary string 方式來說的:
string 型數據(字符串)新的序列化格式爲:
S:<length>:"<value>";
其中 <length> 是源字符串的長度,而非 <value> 的長度。<length> 是非負整數,數字前可以帶有正號(+)。<value> 爲經過轉義之後的字符串。
它的轉義編碼很簡單,對於 ASCII 碼小於 128 的字符(但不包括 \),按照單個字節寫入(與 s 標識的相同),對於 128~255 的字符和 \ 字符,則將其 ASCII 碼值轉化爲 16 進制編碼的字符串,以 \ 作爲開頭,後面兩個字節分別是這個字符的 16 進制編碼,順序按照由高位到低位排列,也就是第 8-5 位所對應的16進制數字字符(abcdef 這幾個字母是小寫)作爲第一個字節,第 4-1 位作爲第二個字節。依次編碼下來,得到的就是 <value> 的內容了。
個人認爲這種大 S 方式的序列化沒有小 s 方式好,對於反序列化來說,小 s 方式可以獲得更快的速度。所以,我認爲 PHP6 的這種修改是 PHP6 的一大敗筆!如果要在其它語言實現 PHP 序列化,只要支持小 s 方式序列化就可以了,使用大 S 方式序列化,將與目前主流的 PHP4 和 PHP5 不兼容。但是反序列化應該實現大 S 方式的反序列化,這樣可以兼容 PHP6 序列化的字符串內容。
 

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> 表示與下標相對應的數組元素的值。
下標的類型只能是整型或者字符串型(包括後面那種 Unicode 字符串型),序列化後的格式跟整型和字符串型數據序列化後的格式相同。
數組元素值可以是任意類型,其序列化後的格式與其所對應的類型序列化後的格式相同。

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;
 
$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 進行數據交換,也可以互相讀懂了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章