序列化和反序列化浅析

简介

序列化和反序列化对于现代的程序员来讲是一个既熟悉又陌生的概念。说熟悉是由于几乎每一个程序员在工做中都直接或间接的使用过它,说陌生是由于大多数程序员对序列化和反序列化的认识仅仅停留在比较一下各类不一样实现的序列化的性能上面,而不多有程序员对序列化和反序列化的设计和实现有深刻的研究。javascript

本文将从序列化和反序列化的设计和实现的入手,来简单讲解一下序列化和反序列化。其中包括如下几个方面:java

  1. 序列化和反序列化的做用程序员

  2. 什么样的数据是可序列化的数据库

  3. 序列化和反序列化的分类编程

  4. 序列化和反序列化的类型映射数组

本文不会涉及到某几种语言的某几种序列化实现的性能对比之类的内容。缓存

序列化和反序列化的做用

咱们在编写程序代码时,一般会定义一些常量和变量,而后再写一堆操做它们的指令。无论是变量仍是常量,它们表示的都是数据。因此简单的说,一个程序就是一堆指令操做一堆数据。网络

可是为了更有效的管理这堆数据,现代的程序设计语言都会引入一个类型系统来对这些数据进行分类管理,而不是让程序员把全部数据都一股脑的当作二进制串来进行操做。编程语言

好比一个常量多是一个数字,一个布尔值,一个字符串,或者是一个由它们构成的数组。而变量一般具备更丰富的类型可使用。甚至你还能够自定义类型。对于面向对象的语言来讲,一个类型表示的不只仅是数据自己,还包括了对这种类型数据的一组操做。编辑器

一个程序能够以源码或者可执行的二进制形式保存在磁盘(或者其它存储介质)上。当你须要执行它时,它会以某种形式被载入内存,而后执行。

一个程序在执行过程当中一般会生成新的数据,这些新的数据一部分是临时的,在内存中,它们转瞬即逝。还有一部分数据可能须要被保存下来,或者被传递到其它的地方去。在这种状况下,可能就会涉及到数据的形式转换的问题。这个把程序运行时的内存数据转换为一种可保存或可传递的数据的过程,咱们就称它为序列化。

这些保存和传递的数据,可能会在某个时间被从新载入内存,但可能会是不一样的进程,或者不一样的程序,甚至不一样的机器上被载入,还原为内存中的具体类型的数据变量,这个从保存的数据还原为具体语言具体类型的数据变量的过程,咱们称它为反序列化。

什么样的数据是可序列化的

什么样的数据可序列化这是一个相对的问题,而不是一个绝对的问题。由于它会受到各类不一样因素的影响。

数据的可还原性

被序列化的数据应该是可还原的。可还原的意思是,一个被序列化的数据在被反序列化后仍然是有意义的。注意,这里说的是有意义,而不是说被反序列化的数据应该跟序列化以前的源数据相同。为了便于理解,咱们来举例说明一下。

指针数据是否可序列化

首先咱们来讨论一下指针类型的数据是否可序列化。

一般咱们认为指针数据是不可序列化的,由于它表示的是一个内存地址,而若是咱们把这个内存地址保存下来,下一次咱们将这个内存地址还原到一个指针变量中时,这个内存地址所指向的位置的数据可能早就不是咱们所须要的数据了,甚至指向的是一个彻底没有意义的数据。因此,在这种状况下,虽然先后两个指针变量的值相同,可是还原以后的指针变量指向的数据已经没有意义了,咱们就称它不具备可还原性。

那么指针数据真的不可序列化吗?若是咱们从须要反序列化的数据有意义这个角度考虑,那么咱们也能够作到对指针数据的序列化。

指针一般分为指向具体类型数据的指针(例如: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 映射。这能够保证序列化以前的数据跟反序列化以后的数据在类型上的彻底一致性。但也因为语言内置类型的丰富性和 1-1 映射的一致性,致使这些语言内置的序列化格式几乎没法作到跨语言实现。

咱们前面也谈到过一个特例,那就是 PHP 序列化,PHP 序列化之因此可以作到跨语言实现,是由于它自己的内置类型很是有限,以致于即便在 PHP 中是 1-1 映射的数据类型还不如其它一些跨语言的序列化支持的数据类型更丰富。

而 JSON 格式,若是把它放到 JavaScript 中,它也是 1-1 映射的。而 JSON 序列化在其它语言中的实现则是多种多样,有的仅支持 1-1 映射,有的则支持 n-m 映射,即使是同一种语言的不一样实现也是如此。

1-1 映射最麻烦的问题是,要么支持的类型不够丰富,要么跨语言方面难以实现。

第一个问题对于原本类型就不是不少的脚本语言来讲一般不是问题,但对于 Java,C# 之类的语言来讲,这就是个问题了。

n-m 映射

n-m 映射能够很好的解决这个问题。

好比序列化格式中不须要为 Array,List,Tuple,Set 定义不一样的类型,而只须要一种通用的列表类型,以后就能够将某种具体语言的 Array,List,Tuple,Set 等具备列表特征的数据都映射为这一种列表类型,在反序列化的时候,则直接反序列化为某种指定的类型。

这样作还有一个额外的好处:当你但愿类型一致的时候,你就能够实现类型一致,而当你不但愿使用一致的类型时,能够直接在序列化和反序列化的过程当中进行类型的转换。而不须要获得了一致的类型以后,再去本身手动转换为另外一种类型。

经过反射方式来实现的序列化和反序列化能够更方便的实现 n-m 映射。而经过代码生成器方式实现的序列化和反序列化则一般只能实现 1-1 映射。所以,经过反射方式来实现的序列化和反序列化具备更好的灵活性。

相关文章
相关标签/搜索