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;
$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 進行資料交換,也可
以互相讀懂了。
8、參考文獻
PHP 3 中關於序列化和還原序列化的原始碼
PHP 4 中關於序列化的原始碼
PHP 4 中關於還原序列化的原始碼
PHP 5 中關於序列化的原始碼
PHP 5 中關於還原序列化的原始碼
PHP 6 中關於序列化的原始碼
PHP 6 中關於還原序列化的原始碼
PHP 手冊中關於序列化和還原序列化的介紹
Using Serialized PHP with Yahoo! Web Services