了解序列化


38

我是一名软件工程师,在与一些同事讨论之后,我意识到我对概念序列化没有很好的了解。据我了解,序列化是将某些实体(例如,OOP中的对象)转换为字节序列的过程,以便可以存储或传输所述实体以供后续访问(“反序列化”过程)。

我遇到的麻烦是:不是所有的变量(无论是像原始int对象还是复合对象)都已由字节序列表示?(当然是,因为它们存储在寄存器,内存,磁盘等中)

那么,什么使序列化成为如此深刻的话题呢?要序列化一个变量,我们不能只将这些字节存储在内存中,然后将它们写入文件吗?我错过了什么错综复杂的事情?


21
对于连续对象,序列化可能很简单。当将对象值表示为指针图时,事情会变得更加棘手,尤其是在所述图具有循环的情况下。

1
@chi:由于连续性无关紧要,所以您的第一句话有些误导。您可能有一个图在内存中碰巧是连续的,并且仍然无法帮助您序列化它,因为您仍然必须(a)检测到它确实是连续的,并且(b)修正内部的指针。我只想说你所说的第二部分。
Mehrdad

@Mehrdad我同意,出于您提及的原因,我的评论并不完全准确。也许无指针/使用指针是更好的区别(即使不是完全准确)
chi

7
您还必须担心硬件上的表示形式。如果我4 bytes在PDP-11上序列化一个int ,然后尝试将这四个字节读入我的macbook的内存中,则它们的编号不相同(由于Endianes)。因此,您必须将数据标准化为可以解码的表示形式(这是序列化)。如何序列化数据还需要权衡速度/灵活性(人机可读)。
马丁·约克

如果您将Entity Framework与许多深度连接的导航属性一起使用怎么办?在一种情况下,您可能希望序列化导航属性,而在另一种情况下,请将其保留为空(因为您将基于序列化的父对象中的ID从数据库中重新加载该实际对象)。这只是一个例子。有许多。
ErikE

Answers:


40

如果您具有复杂的数据结构,则其在内存中的表示通常可能分散在整个内存中。(例如,考虑一个二叉树。)

相反,当您要将其写入磁盘时,可能希望将其表示为(希望很短的)连续字节序列。这就是序列化为您所做的。


27

我遇到的麻烦是:不是所有的变量(例如int或复合对象之类的原始变量)都已经由字节序列表示了吗?(因为它们存储在寄存器,内存,磁盘等中,所以它们是当然的)

那么,什么使序列化成为如此深刻的话题呢?要序列化一个变量,我们不能只将这些字节存储在内存中,然后将它们写入文件吗?我错过了什么错综复杂的事情?

考虑C中的对象图,其节点定义如下:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

在运行时,整个对象Node图将散布在内存空间中,并且可以从许多不同的节点指向同一节点。

您不能简单地将内存转储到文件/流/磁盘并进行序列化,因为指针值(即内存地址)无法反序列化(因为在转储回载时这些内存位置可能已被占用)进入内存)。简单地转储内存的另一个问题是,您最终将存储各种不相关的数据和未使用的空间-在x86上,一个进程最多具有4GiB的内存空间,而OS或MMU仅大致了解实际的内存是什么是否有意义(基于分配给进程的内存页),因此Notepad.exe每当我要保存文本文件时将4GB的原始字节转储到我的磁盘上似乎有点浪费。

版本控制的另一个问题是:如果Node在第1天序列化图形,然后在第2天,向其中添加另一个字段Node(例如另一个指针值或原始值),然后在第3天,从第一天?

您还必须考虑其他事项,例如字节序。尽管表面上是由相同程序(Word,Photoshop等)制作的,但MacOS和IBM / Windows / PC文件在1980年代和1990年代彼此不兼容的主要原因之一是因为在x86 / PC上是多字节整数值在Mac上按小端顺序保存,但按大端顺序保存-而且该软件在构建时并未考虑跨平台的可移植性。如今,得益于改进的开发人员教育以及我们日益多样化的计算世界,事情变得更好了。


2
出于安全原因,将所有内容转储到进程存储空间中也将是可怕的。一个程序之夜在内存中同时具有1)一些公共数据和2)密码,秘密随机数或私钥。序列化前者时,不希望透露有关后者的任何信息。

8
关于此主题的一个非常有趣的注释:为什么Microsoft Office文件格式如此复杂?
击中

15

棘手的问题实际上已经在单词“ 串行化”中描述过。

问题基本上是:如何将任意复杂对象的任意复杂互连的循环有向图表示为字节的线性序列?

想一想:线性序列有点像退化的有向图,其中每个顶点都恰好具有一个输入和输出边缘(“无输入边缘的“第一个顶点”和无输出边缘的“最后一个顶点”除外) 。而一个字节显然不是那么复杂的对象

因此,当我们从一个任意复杂的图形转到一个更为严格的“图形”(实际上只是一个列表),从任意复杂的对象转换为简单字节时,信息丢失,这是合理的。不能以某种方式对“外部”信息进行编码。这正是序列化的作用:将复杂信息编码为简单的线性格式。

如果您熟悉YAML,则可以看看锚点别名功能,这些功能使您可以表示序列化中“同一对象可能出现在不同位置”的想法。

例如,如果您有以下图形:

A → B → D
↓       ↑
C ––––––+

您可以将其表示为YAML中的线性路径列表,如下所示:

- [&A A, B, &D D]
- [*A, C, *D]

您也可以将其表示为邻接列表或邻接矩阵,或者表示为一对,其第一个元素是一组节点,第二个元素是一组节点对,但是在所有这些表示中,您都需要向后和向前引用现有节点(即指针)的方法,通常在文件或网络流中没有这些指针。最后,您所拥有的只是字节。

(BTW意味着上述YAML文本文件本身也需要“序列化”,这就是各种字符编码和Unicode传输格式用于的内容……它不是严格的“序列化”,仅是编码,因为文本文件已经是串行文件了。 /代码点的线性列表,但您可以看到一些相似之处。)


13

其他答案已经解决了复杂的对象图,但是值得指出的是,序列化原语也是很简单的。

为了具体起见,使用C基本类型名称考虑:

  1. 我序列化了一个long。后来我反序列化,但是......在不同的平台,现在有的时候longint64_t而非int32_t存储我。因此,我需要非常小心我存储的每种类型的确切大小,或者存储一些描述每个字段的类型和大小的元数据。

    请注意,在将来重新编译后,此不同平台可能只是同一平台。

  2. 我序列化一个int32_t。一段时间后,我反序列化了它,但是...在另一个平台上,现在值已损坏。可悲的是,我将价值保存在一个大端的平台上,并加载到一个小端的平台上。现在,我需要为我的格式建立一个约定,或者添加更多描述每个文件/流/任何东西的字节序的元数据。并且,当然,实际执行适当的转换。

  3. 我序列化一个字符串。这次,一个平台使用charUTF-8,另一个使用wchar_tUTF-16。

因此,我声称即使对于连续内存中的基元而言,合理质量的序列化也不是小事。您需要记录或使用内联元数据描述许多编码决策。

对象图在此之上增加了另一层复杂性。


6

有多个方面:

同一程序的可读性

您的程序已经以某种方式将数据存储为内存中的字节。但是它可能被任意地分散在不同的寄存器中,并且指针在其较小的块之间来回移动。 。只需考虑一个链接的整数列表。每个列表元素可能存储在完全不同的位置,并且将列表保持在一起的所有元素都是从一个元素到下一个元素的指针。如果要按原样获取这些数据,并尝试将其复制到另一台运行相同程序的计算机上,则会遇到问题:

  1. 首先,地址存储在一台机器上的寄存器可能已经用于另一台机器上完全不同的东西(有人正在浏览堆栈交换,浏览器已经吃光了所有内存)。因此,如果您只是覆盖那些寄存器,那就再见了。因此,您将需要在结构中重新安排指针以适合第二台计算机上可用的地址。当您以后尝试在同一台计算机上重新加载数据时,会出现相同的问题。
  2. 如果某些外部组件指向您的结构或您的结构具有指向外部数据的指针,而您没有传输该怎么办?Segfaults无处不在!这将成为调试的噩梦。

另一个程序的可读性

假设您设法在另一台计算机上分配正确的地址,以便将数据放入其中。如果数据是由该计算机上的单独程序(不同语言)处理的,则该程序可能对数据有完全不同的基本理解。假设您有带有指针的C ++对象,但是您的目标语言甚至不支持该级别的指针。同样,您最终没有在第二个程序中处理该数据的绝妙方法。您最终在内存中获得了一些二进制数据,但是随后,您需要编写一些额外的代码来包装这些数据,并以某种方式将其转换为目标语言可以使用的代码。听起来像是反序列化,只是您现在的起点是散布在主内存中的奇怪对象,这对于不同的源语言是不同的,而不是结构明确的文件。当然,同样的事情,如果您尝试直接解释包含指针的二进制文件,则需要以每种可能的方式编写解析器,以另一种语言表示内存中的数据。

人类的可读性

基于Web的序列化的两种最杰出的现代序列化语言(xml,json)是人类容易理解的。取代了二进制文件,没有了读取数据的程序,数据的实际结构和内容仍然清晰可见。这具有多个优点:

  • 调试更轻松->如果服务管道中存在问题,则只需查看来自一项服务的数据并检查它是否有意义(作为第一步);当您首先编写导出接口时,您还可以直接查看数据是否看起来像您认为的那样。
  • 可归档性:如果您将数据当作纯二进制文件堆放了,并且松散了用于解释它的程序,那么您就松散了数据(否则,您将需要花费大量时间才能在其中实际找到内容);如果您的序列化数据是人类可读的,则可以轻松地将其用作存档或为新程序编写自己的导入器
  • 以这种方式序列化的数据的声明性也意味着,它完全独立于计算机系统及其硬件;您可以将其加载到结构完全不同的量子计算机中,或者用其他事实感染外星人AI,以便它意外地飞入下一个太阳(Emmerich,如果您阅读此书,那么如果您在下一个7月4日使用该想法,将是一个不错的参考电影)

我的数据可能主要在主存储器中,而不是寄存器中。如果我的数据适合寄存器,那么序列化甚至不是问题。我认为您误解了寄存器的含义。
David Richerby

确实,我在这里过于宽松地使用了注册一词。但要点是,您的数据可能包含指向地址空间的指针,以标识其自身的组件或引用其他数据。它是物理寄存器还是主存储器中的虚拟地址都没有关系。
Frank Hopkins

不,您完全错误地使用了“注册”一词。您要调用的寄存器与实际寄存器在内存层次结构中完全不同。
David Richerby

6

除了其他答案所说的以外:

有时您想要序列化非纯数据。

例如,考虑文件句柄或与服务器的连接。即使文件句柄或套接字是int,该数字在下次程序运行时也没有意义。要正确地重新创建包含此类操作的对象,您需要重新打开文件并重新创建连接,并确定如果失败则该怎么办。

如今,许多语言都支持在对象中存储匿名函数,例如onBlah()Java 语言中的处理程序。这具有挑战性,因为这样的代码可以包含对其他数据的引用,而这些数据又需要序列化。(然后是跨平台方式序列化代码的问题,对于解释型语言而言,这显然更容易。)尽管如此,即使仅支持该语言的子集,它仍然非常有用。没有很多序列化机制尝试对代码进行序列化,但是请参见serialize-javascript

如果您要序列化一个对象,但其中包含序列化机制不支持的对象,则需要以一种解决此问题的方式重写代码。例如,当数量有限的可能函数时,可以使用枚举代替匿名函数。

通常,您希望序列化的数据简洁。

如果您正在通过网络发送数据,或者甚至将其存储在磁盘上,那么保持大小很小就很重要。实现此目的最简单的方法之一就是丢弃可以重建的信息(例如,丢弃高速缓存,哈希表和同一数据的备用表示形式)。

当然,程序员必须手动选择要保存的内容和要丢弃的内容,并确保在重新创建对象时重新构建内容。

考虑一下保存游戏的行为。对象可能包含许多指向图形数据,声音数据和其他对象的指针。但是,这些东西大部分都可以从游戏数据文件中加载,而无需存储在保存文件中。丢弃它可能很费力,因此经常遗留的东西很少。我在自己的时间里对一些保存文件进行了十六进制编辑,并发现了明显多余的数据,例如文本项说明。

有时空间并不重要,但可读性却很重要,在这种情况下,您可以使用ASCII格式(可能是JSON或XML)。


3

让我们定义一个字节序列实际上是什么。字节序列由称为长度的非负整数和一些任意函数/对应组成,该函数将至少为零且小于长度的任何整数i映射到字节值(0到255的整数)。

您在典型程序中处理的许多对象都不是这种形式,因为这些对象实际上是由许多不同的内存分配组成的,这些内存分配位于RAM中的不同位置,并且彼此之间可能被数百万字节的内容分隔开不在乎。试想一下一个基本的链表:链表中的每个节点都是字节序列,是的,但是这些节点在计算机内存中位于许多不同的位置,并且它们与指针相连。或者只是考虑一个简单的结构,该结构具有指向可变长度字符串的指针。

我们之所以要将数据结构序列化为字节序列,通常是因为我们希望将其存储在磁盘上或将其发送到其他系统(例如,通过网络)。如果您尝试将指针存储在磁盘上或将其发送到其他系统,则将毫无用处,因为读取该指针的程序将具有一组不同的可用存储区域。


1
我不确定这是否是序列的好定义。大多数人会把一个序列定义为一个序列:一堆又一堆的东西。根据您的定义,int seq(int i) { if (0 <= i < length) return i+1; else return -1;}是一个序列。那么我该如何将其存储在磁盘上?
David Richerby

1
如果长度为4,I存储四个字节的文件与内容:1,2,3,4,
大卫格雷森

1
@DavidRicherby他的定义等同于“一连串的事物”,它只是比您的直观定义更数学和精确的定义。请注意,您的函数不是一个序列,因为要拥有一个序列,您需要该函数另一个称为长度的整数。
user253751

1
@FreshAir我的意思是顺序是1、2、3、4、5。我写下的是一个函数。函数不是序列。
David Richerby

1
我已经提出了一种将函数写入磁盘的简单方法:对于每个可能的输入,都存储输出。我想也许您还是不明白,但我不确定该说些什么。您是否知道在嵌入式系统中通常将昂贵的函数转换sin成查找表,查找表是一个数字序列?您是否知道您关心的功能与此功能相同? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } 您为什么准确地说我的四字节文件表示不充分?
David Grayson

2

复杂性反映了数据和对象本身的复杂性。这些对象可以是真实世界的对象,也可以是仅计算机的对象。答案就是名字。序列化是多维对象的线性表示。除了碎片化的RAM之外,还有许多其他问题。

如果您可以展平12个5维数组和一些程序代码,则序列化还允许您在机器之间传输整个计算机程序(和数据)。诸如RMI / CORBA之类的分布式计算协议广泛使用序列化来传输数据和程序。

考虑一下您的电话费。它可能是一个对象,包括您的所有通话(字符串列表),应付金额(整数)和国家/地区。或者,您的电话账单可能是上述内容的由内而外,并且包含与您的姓名相关联的不连续的分项电话。每个扁平化的外观将有所不同,反映出您的电话公司是如何编写该软件版本的,以及面向对象的数据库从未成功的原因。

结构的某些部分甚至可能根本不在内存中。如果您具有延迟缓存,则对象的某些部分可能仅被引用到磁盘文件,并且仅在访问该特定对象的那部分时才加载。这在严肃的持久性框架中很常见。BLOB是一个很好的例子。盖蒂图片社可能会存储一张Fidel Castro巨大的数兆字节的图片,以及一些元数据,例如图片的名称,租赁费用和图片本身。您可能不希望每次都将200 MB的图片加载到内存中,除非您实际看着他。序列化后,整个文件将需要200MB以上的存储空间。

有些对象甚至根本无法序列化。在Java编程领域,您可以拥有一个表示图形屏幕或物理串行端口的编程对象。序列化它们中的任何一个都没有真正的概念。您将如何通过网络将端口发送给其他人?

诸如密码/加密密钥之类的东西不应该被存储或传输。可以将它们标记为此类(易失性/瞬态等),并且序列化过程将跳过它们,但它们可以存在于RAM中。忽略这些标签是无意间以纯ASCII发送/存储加密密钥的方式。

这个和其他答案就是为什么它很复杂。


2

我遇到的麻烦是:不是所有的变量(例如int或复合对象之类的原始变量)都已经由字节序列表示了吗?

对,他们是。这里的问题是这些字节的布局。一个简单的int可以是2、4或8位长。它可以是大端或小端。它可以是无符号的,也可以用1的补码进行签名,甚至可以使用诸如负数之类的一些超级奇异位编码。

如果只是int从内存中转储二进制文件,并将其称为“序列化”,则必须附加几乎整个计算机,操作系统和程序,才能对其进行反序列化。或者至少是它们的精确描述。

那么,什么使序列化成为如此深刻的话题呢?要序列化一个变量,我们不能只将这些字节存储在内存中,然后将它们写入文件吗?我错过了什么错综复杂的事情?

一个简单对象的序列化几乎是根据一些规则将其记录下来的。这些规则很多,但并不总是很明显。例如,xs:integer用XML编写的以10为基数。不是以16为基数,不是以9为基数,而是10。这不是一个隐藏的假设,而是一条实际规则。这样的规则使序列化成为序列化。因为几乎没有关于程序在内存中的位布局的规则

那只是冰山一角。让我们以这些最简单的原语序列为例:C struct。你可能会认为

struct {
short width;
short height;
long count;
}

在给定的计算机+ OS上有定义的内存布局?好吧,事实并非如此。根据当前#pragma pack设置,编译器将填充这些字段。在32位编译的默认设置下,两者都shorts将填充为4个字节,因此它们struct实际上将在内存中具有3个4字节的字段。因此,现在,您不仅需要指定short16位长,而且是一个整数,以1的补码负,大或小尾数表示。您还必须写下编译程序时使用的结构打包设置。

这几乎就是序列化的意义:制定一套规则,并遵守这些规则。

然后可以扩展这些规则,以接受甚至更复杂的结构(如可变长度列表或非线性数据),添加诸如人类可读性,版本控制,向后兼容性和错误纠正等功能,但是即使编写单个代码int也已经足够复杂,如果您只希望确保您能够可靠地读回它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.