序列化和反序列化對於現代的程序員來講是一個既熟悉又陌生的概念。說熟悉是由於幾乎每一個程序員在工做中都直接或間接的使用過它,說陌生是由於大多數程序員對序列化和反序列化的認識僅僅停留在比較一下各類不一樣實現的序列化的性能上面,而不多有程序員對序列化和反序列化的設計和實現有深刻的研究。javascript
本文將從序列化和反序列化的設計和實現的入手,來簡單講解一下序列化和反序列化。其中包括如下幾個方面:java
本文不會涉及到某幾種語言的某幾種序列化實現的性能對比之類的內容。程序員
咱們在編寫程序代碼時,一般會定義一些常量和變量,而後再寫一堆操做它們的指令。無論是變量仍是常量,它們表示的都是數據。因此簡單的說,一個程序就是一堆指令操做一堆數據。數據庫
可是爲了更有效的管理這堆數據,現代的程序設計語言都會引入一個類型系統來對這些數據進行分類管理,而不是讓程序員把全部數據都一股腦的當作二進制串來進行操做。編程
好比一個常量多是一個數字,一個布爾值,一個字符串,或者是一個由它們構成的數組。而變量一般具備更豐富的類型可使用。甚至你還能夠自定義類型。對於面向對象的語言來講,一個類型表示的不只僅是數據自己,還包括了對這種類型數據的一組操做。數組
一個程序能夠以源碼或者可執行的二進制形式保存在磁盤(或者其它存儲介質)上。當你須要執行它時,它會以某種形式被載入內存,而後執行。緩存
一個程序在執行過程當中一般會生成新的數據,這些新的數據一部分是臨時的,在內存中,它們轉瞬即逝。還有一部分數據可能須要被保存下來,或者被傳遞到其它的地方去。在這種狀況下,可能就會涉及到數據的形式轉換的問題。這個把程序運行時的內存數據轉換爲一種可保存或可傳遞的數據的過程,咱們就稱它爲序列化。網絡
這些保存和傳遞的數據,可能會在某個時間被從新載入內存,但可能會是不一樣的進程,或者不一樣的程序,甚至不一樣的機器上被載入,還原爲內存中的具體類型的數據變量,這個從保存的數據還原爲具體語言具體類型的數據變量的過程,咱們稱它爲反序列化。編程語言
什麼樣的數據可序列化這是一個相對的問題,而不是一個絕對的問題。由於它會受到各類不一樣因素的影響。編輯器
被序列化的數據應該是可還原的。可還原的意思是,一個被序列化的數據在被反序列化後仍然是有意義的。注意,這裏說的是有意義,而不是說被反序列化的數據應該跟序列化以前的源數據相同。爲了便於理解,咱們來舉例說明一下。
首先咱們來討論一下指針類型的數據是否可序列化。
一般咱們認爲指針數據是不可序列化的,由於它表示的是一個內存地址,而若是咱們把這個內存地址保存下來,下一次咱們將這個內存地址還原到一個指針變量中時,這個內存地址所指向的位置的數據可能早就不是咱們所須要的數據了,甚至指向的是一個徹底沒有意義的數據。因此,在這種狀況下,雖然先後兩個指針變量的值相同,可是還原以後的指針變量指向的數據已經沒有意義了,咱們就稱它不具備可還原性。
那麼指針數據真的不可序列化嗎?若是咱們從須要反序列化的數據有意義這個角度考慮,那麼咱們也能夠作到對指針數據的序列化。
指針一般分爲指向具體類型數據的指針(例如:int *
, string *
)和指向不明類型數據的指針(例如 void *
)。
對於前者,若是咱們但願序列化的數據包含指向的具體類型的數據,而且在反序列化以後,可以還原爲一個指向該具體類型數據的指針,且指向的數據值跟源值相同的話,那麼咱們實際上是能夠作到的。雖然,還原以後的指針所指向的內存地址,跟源指針指向的內存地址可能徹底不同,可是它指向的數據是有意義的,且是咱們指望的,那麼這種狀況下,咱們也能夠稱這個指針數據是可還原的。
對於後者,若是咱們沒有所指向數據的具體信息,那就沒有辦法對指向的數據進行保存。因此這種類型的指針也就沒辦法進行序列化了。
另外,還有一種特殊的指針類型,它保存的並非一個具體的內存地址,而是一個相對的偏移量,好比 uintptr_t
類型就常做此用,這種時候,對它的值序列化和反序列化以後,獲得的值仍然是一樣的相對偏移量值,在這種狀況下,反序列化後的數據就是有意義的,因此,這種指針數據也具備可還原性。
從上面的分析,咱們能夠看出,指針類型是否可序列化,取決於咱們想要什麼意義的反序列化數據。
對於資源類型,有些語言有明確的定義,好比 PHP,而有些語言則沒有明確的定義。但大體上咱們能夠認爲一個打開的文件對象,一個打開的數據庫鏈接,一個打開的網絡套接字,以及諸如此類跟外部資源相關的數據類型,均可以被稱做資源類型。
對於資源類型咱們一般認爲它們都是不可序列化的,哪怕表示該類型的結構體中的全部字段都是可序列化的基本類型數據。緣由是這些資源類型中保存的數據是跟當前打開的資源相關的,這些數據若是複製到其它的進程,或者其它的機器中去以後,這些資源類型中保存的數據就失去了意義。
對於資源類型的一部分屬性數據,好比文件名,數據庫地址,網絡套接字地址,它們能夠在不一樣的進程、不一樣的機器之間傳遞以後,仍然表示原有的意義。
可是一般的序列化程序是不會對資源類型作這樣的序列化操做的,由於序列化程序對資源類型序列化時,並不能假定用戶須要的僅僅是這些信息,並且若是用戶須要的真的就僅僅是這些信息的話,那用戶徹底能夠明確的只序列化這些數據,而不是對整個資源類型作序列化操做。
可是有些特殊的資源,好比內存流,文件流等。不一樣的序列化實現可能對待它們的方式也不一樣。有些序列化實現認爲這些資源類型一樣不可序列化。而有些序列化實現則認爲能夠將資源自己一塊兒序列化,好比內存流中的數據會被做爲序列化數據的主體進行序列化,在反序列化時,被反序列化爲另一個內存流對象,雖然是兩個不一樣的資源,可是資源中的數據是相同的。
一個數據可否被序列化,還要看所使用的序列化格式是否支持。
對於基本類型的數據來講,幾乎全部的序列化格式都支持。可是對於有些採用代碼生成器方式實現的序列化來講,它們可能只支持經過 IDL 生成的代碼中所定義的類型的序列化,而不支持對語言內置的單個原生類型數據變量的序列化,也不支持經過普通方式定義的自定義類型數據的序列化。好比 Protocol Buffers 就是這樣。
對於複雜類型,好比 map 這種類型,有些序列化格式只支持 Key 爲字符串類型的 map 數據的序列化。而不支持其它 Key 類型的 map 數據的序列化。好比 JSON 就是這樣。
還有一種複雜類型數據是帶有循環引用結構的數據,好比下面這個 JavaScript 代碼中定義的這個數組 a
:
var a = []; a[0] = a;
它的第一個元素引用了本身,這就產生了循環引用。對於這種類型的數據,不少的序列化格式也是不支持的,好比 JSON,Msgpack 都不支持這種類型數據的序列化。
可是上面所說的狀況,並非全部的序列化格式都不支持,好比 Hprose 對上面所說的全部類型都支持。
以上這些限制都是序列化格式自己形成的。
對於同一種序列化格式,即使是在同一種語言中,也可能存在着多種不一樣的實現,好比對於 JSON 序列化來講,它的 Java 版本的實現甚至有上百種。這些不一樣的實現各有特點,也各有各的限制,甚至互不兼容。有些實現可能僅僅支持幾種特別定義的類型。有些則對語言內置的類型提供了很好的支持。
還有一些序列化格式跟特定語言有緊密的綁定關係,所以沒法作到跨語言的序列化和反序列化,好比 Java 序列化,.NET 的 Binary 序列化,Go 語言的 Gob 序列化格式就只能支持特定的語言。
並且即使是這種針對特定語言的序列化也不是支持該語言的全部類型。好比:Java 序列化對於 class 類型只支持實現了 java.io.Serializable
接口的類型;.NET Binary 序列化則只支持標記了 System.SerializableAttribute
屬性的類型。
因此,咱們不能想固然的認爲,一個數據支持某一種序列化,就必定支持其它類型的序列化。這種假設是不成立的。
序列化和反序列化的格式多種多樣,它們之間的主要區別能夠大體分這樣幾類:
首先從可讀性角度,大體可分爲文本序列化和二進制序列化兩種,可是也有一些序列化格式介於二者之間,咱們將它們暫稱爲半文本序列化。
XML 和 JSON 是你們最多見的兩種文本序列化格式。
文本序列化的數據都是使用人類可讀的字符表示的,就像大部分編程語言同樣。並且容許包含多餘的空白,以增長可讀性。固然也能夠表示爲緊湊編碼形式,以利於減小存儲空間和傳輸流量。
文本序列化除了可讀性還具備可編輯性,所以,文本序列化格式也常常被用於做爲配置文件的存儲格式。這樣,使用普通的文本編輯器就能夠方便的編輯這種配置文件。
文本序列化在表示數字時,一般採用人類可讀的十進制數(包括小數和科學計數法)的字符串形式,這除了具備可讀性之外,還有另一個好處,就是能夠方便的表示大整數或者高精度小數。
二進制序列化的的數據不具備可讀性,可是一般比文本序列化格式更加緊湊,並且在解析速度上也更有優點,固然實際的解析速度還跟具體實現有很大的關係,因此這也不是絕對的。
由於它們自己不具備可讀性,因此在實際使用時,若是要想查看這些數據,就須要藉助一些工具將它們解析爲可讀信息以後才能使用。在這方面,它們相對於文本序列化具備明顯的劣勢。
二進制序列化表示數字時,一般會使用定長或者變長的二進制編碼方式,這雖然有利於更快的編碼和解析編程語言中的基本數字類型,可是卻不能表示大整數和高精度小數。
Protocol Buffers,Msgpack,BSON,Hessian 等格式是二進制序列化格式的表明。
半文本序列化格式一般兼具文本序列化的可讀性和二進制序列化的性能。
半文本序列化的數據也使用人類可讀的字符表示,具備必定的可讀性,可是半文本序列化是空白敏感的,所以它們不能像文本序列化那樣在序列化數據中添加空白。
半文本序列化格式採用緊湊編碼形式,並且一般會採用跟二進制編碼相似的TLV(Type-Length-Value)編碼方式,所以具備比文本序列化更高效的解析速度,固然實際解析效率也跟具體實現有關。
半文本序列化格式中對本來的二進制字符串數據仍然按照二進制字符串的格式保存,而不會像文本序列化格式同樣,須要將它們轉換爲 Base64 格式的文本。對於二進制字符串來講,無論是轉爲 Base64 格式的文本仍是本來的樣子,都不具備可讀性,所以,直接以原格式保存,並不損失可讀性,可是卻能夠增長解析效率。
半文本序列化格式在表示字符串時不會像文本序列化那樣在字符串中間增長轉義字符,或者將本來的字符用轉義符號表示,所以,半本文序列化格式中的字符串反而比文本序列化的字符串具備更好的可讀性。
半文本序列化格式在數字編碼上具備跟文本序列化格式同樣的特色。
Hprose,PHP 序列化格式是半文本序列化的表明。
若是序列化數據中包含有數據類型的元信息,或者數據的表示形式同時能夠反映出它的類型,那麼這種序列化格式就是自描述的。自描述的序列化格式,能夠在不借助外部描述的狀況下,進行解析。
文本序列化和半文本序列化基本上都是自描述的。二進制序列化格式中,大部分也是自描述的。
自描述序列化格式不依賴外描述文件是它的優點,在一些應用場景下,這具備不可替代的優越性。但也由於包含了元信息,致使它的數據大小一般要比非自描述序列化的數據大一些。
像 XML,JSON,Hprose,Hessian,Msgpack 都是自描述類型的序列化格式。
非自描述序列化的數據在體積上更小,可是由於捨棄了自描述性,使得這種序列化數據在離開外部描述以後,就沒法再被使用。
Protocol Buffers 是典型的非自描述類型的序列化格式的表明。
序列化和反序列化的很大一部分特徵是由它們的實現決定的。關於序列化一般是使用代碼生成或者反射的方式來實現,而對於反序列化除了這兩種方式以外,還有將序列化數據解析爲語法樹的方式,這種方式實際上並不算反序列化,但一般能夠更快的查找和獲取文本序列化數據中某個節點的值。
採用代碼生成方式實現序列化的好處是能夠不依賴編程語言自己運行時中的元數據信息,這樣即便某個語言(好比 C/C++)的運行時中自己沒有包含足夠的元數據時,也能夠方便的進行序列化和反序列化。
採用代碼生成方式實現序列化的另外一個好處是,由於不使用反射,序列化和反序列化的速度一般會比基於反射實現的序列化反序列化更快一些。
可是採用代碼生成方式實現的序列化的缺點也很明顯,好比對支持的數據類型限制比較嚴格,使用起來比較麻煩,須要編寫 IDL 文件,在類型映射上比較死板,一般只能實現 1-1 的映射(這個咱們後面再談),類型升級時,會產生兼容性問題等等。
基於動態反射來實現序列化和反序列化能夠作到更好的類型支持,好比語言的內置類型和普通方式編寫的自定義類型的數據均可以被序列化和反序列化,並且無需編寫 IDL 文件就能夠實現動態序列化,類型映射也更加靈活,能夠實現 n-m 的映射,類型升級時,能夠避免產生兼容性問題。
但一般基於反射實現的序列化和反序列化的速度要比採用代碼生成方式的序列化和反序列化要慢一些,可是這也不是絕對的,由於在實現中,可經過一些其它的手段來提高性能。
例如採用緩存的方式,對於那些須要反射才能得到的元信息進行緩存,這樣在獲取元信息時能夠避免反射而直接使用緩存的元信息來加快序列化速度。還可使用動態的字節碼生成方式,好比在 Java 中使用 ASM 技術來動態生成序列化和反序列化的代碼,在 .NET 中使用 Emit 技術也能夠實現一樣的功能。而對於 C、C++、Rust 等語言能夠採用宏和模板的方式在編譯期生成具體類型的序列化和反序列化的代碼,對於 D、Nim 等語言則能夠採用編譯期反射和編譯期代碼執行功能在編譯期動態生成具體類型的序列化和反序列化代碼,經過這些手段,既能夠得到傳統的代碼生成器方式的序列化和反序列化的性能,又能夠避免代碼生成器的缺陷。
例如 Hprose for .NET 就採用上面提到的元數據緩存 + Emit 動態代碼生成的優化手段,使得它的序列化和反序列化速度遠遠超過 Protocol Buffers 的速度。
並非全部的序列化格式都是跨語言的。即便是跨語言的序列化格式,在跨語言的能力上也有所不一樣。
大部分語言內置的序列化格式都屬於特定語言專有的序列化。例如 Java 的序列化,.NET 的 Binary 序列化,Go 的 Gob 序列化都屬於這一種。
但也有特例,好比 PHP 序列化,本來是 PHP 語言專有的序列化格式,但由於它的格式比較簡單,所以也有一些其它語言上的 PHP 序列化的第三方實現。但終究 PHP 序列化格式跟 PHP 語言的關係更加緊密,因此在其餘語言中使用 PHP 序列化時相對於其它跨語言的序列化格式或多或少的會有一些不方便的地方。
文本序列化格式每每具備更好的跨語言特徵。好比 XML,JSON 等序列化格式,對於不一樣的語言都有不少的實現來支持。
還有一些半文本或二進制序列化格式也是爲跨語言而設計的,好比 Hprose,Protocol Buffers,MsgPack 等,它們也具備很好的跨語言能力。
但多數二進制序列化格式在跨語言方面有不少限制。
若是編程語言中的數據類型跟序列化格式中的數據類型有且只有惟一的映射關係,咱們就把這種類型映射關係稱爲 1-1 映射。
若是在序列化時,編程語言中的多種數據類型被映射爲一種序列化格式的類型,而且在反序列化時,一種序列化類型能夠被反序列化爲編程語言中的多種類型,那麼這種類型映射關係稱爲 n-m 映射。
固然還存在其它的狀況,好比多種序列化類型被反序列化爲編程語言中的同一種類型,再好比編程語言中的全部類型跟序列化類型中的某個類型都不存在映射關係,等等。這些其它狀況,咱們也把它們歸到 1-1 映射中。
1-1 映射仍是 n-m 映射,除了跟序列化格式有關之外,還跟具體的語言實現有很大的關係。
語言內置的序列化和反序列化實現通常都是 1-1 映射。這能夠保證序列化以前的數據跟反序列化以後的數據在類型上的徹底一致性。但也因爲語言內置類型的豐富性和 1-1 映射的一致性,致使這些語言內置的序列化格式幾乎沒法作到跨語言實現。
咱們前面也談到過一個特例,那就是 PHP 序列化,PHP 序列化之因此可以作到跨語言實現,是由於它自己的內置類型很是有限,以致於即便在 PHP 中是 1-1 映射的數據類型還不如其它一些跨語言的序列化支持的數據類型更豐富。
而 JSON 格式,若是把它放到 JavaScript 中,它也是 1-1 映射的。而 JSON 序列化在其它語言中的實現則是多種多樣,有的僅支持 1-1 映射,有的則支持 n-m 映射,即使是同一種語言的不一樣實現也是如此。
1-1 映射最麻煩的問題是,要麼支持的類型不夠豐富,要麼跨語言方面難以實現。
第一個問題對於原本類型就不是不少的腳本語言來講一般不是問題,但對於 Java,C# 之類的語言來講,這就是個問題了。
n-m 映射能夠很好的解決這個問題。
好比序列化格式中不須要爲 Array,List,Tuple,Set 定義不一樣的類型,而只須要一種通用的列表類型,以後就能夠將某種具體語言的 Array,List,Tuple,Set 等具備列表特徵的數據都映射爲這一種列表類型,在反序列化的時候,則直接反序列化爲某種指定的類型。
這樣作還有一個額外的好處:當你但願類型一致的時候,你就能夠實現類型一致,而當你不但願使用一致的類型時,能夠直接在序列化和反序列化的過程當中進行類型的轉換。而不須要獲得了一致的類型以後,再去本身手動轉換爲另外一種類型。
經過反射方式來實現的序列化和反序列化能夠更方便的實現 n-m 映射。而經過代碼生成器方式實現的序列化和反序列化則一般只能實現 1-1 映射。所以,經過反射方式來實現的序列化和反序列化具備更好的靈活性。