【问题标题】:What does data serialization do?数据序列化有什么作用?
【发布时间】:2012-08-17 07:06:41
【问题描述】:

我很难理解序列化是什么以及做什么。

让我简化一下我的问题。我的 c/c++ 程序中有一个 struct info,我可以将这个 struct 数据存储到一个文件 save.bin 中,或者通过套接字将它发送到另一台计算机。

struct info {
    std::string name;
    int age;
};

void write_to_file()
{
    info a = {"Steve", 10};
    ofstream ofs("save.bin", ofstream::binary);
    ofs.write((char *) &a, sizeof(a));   // am I doing it right?
    ofs.close();
}

void write_to_sock()
{
    // I don't know about socket api, but I assume write **a** to socket is similar to file, isn't it?
}

write_to_file 将简单地将struct info 对象a 保存到磁盘,使这些数据持久化,对吗?而且写到socket上也差不多吧?

在上面的代码中,我不认为我使用了数据序列化,但是数据a 无论如何都会在save.bin 中持久化,对吧?

问题

  1. 那么序列化的意义何在?我这里需要吗?如果是,我应该如何使用它?

  2. 我一直认为任何类型的文件,.txt/.csv/.exe/...,在内存中都是01的位,这意味着它们自然具有二进制表示,所以我们不能简单地通过套接字直接发送这些文件吗?

非常感谢代码示例。

【问题讨论】:

  • 没有用,如果客户端和服务器都由你维护。!!!!
  • @Anonymous,如果两者都不是我维护的?
  • 你必须遵守标准,这就是序列化发挥作用的地方。
  • @Anonymous,你能用一些代码举例说明吗?

标签: c++ serialization


【解决方案1】:

但是数据 a 无论如何都会在 save.bin 中持久化,对吗?

不!您的结构包含std::string。确切的实现(以及通过转换为char* 获得的二进制数据不是由标准定义的,但实际的字符串数据将始终在类框架之外的某个地方辞职,堆分配,所以你不能保存它数据很容易。通过正确完成序列化,字符串数据将写入类的其余部分也结束的位置,因此您将能够从文件中读取它。这就是您需要序列化的原因。

怎么做:你必须以某种方式对字符串进行编码,最简单的方法是先写它的长度,然后写字符串本身。在读回文件时,首先读回长度,然后将该字节数读入一个新的字符串对象。

我一直认为任何类型的文件,.txt/.csv/.exe/...,在内存中都是 01 的位

是的,但问题是没有普遍定义哪个位代表数据结构的哪个部分。特别是有little-endian and big-endian architectures,它们以“相反的方式”存储位。如果你天真地读出一个用不匹配架构编写的文件,你显然会得到垃圾。

【讨论】:

  • +1 获取详细答案。通过“以某种方式编码字符串”,你的意思是我最好使用 utf-8 或类似的东西对字符串进行编码?但我真的需要这样做吗?我的意思是就像你说的那样,我可以简单地将字符串的长度写入二进制文件以指示字符串的长度,或者我可以在写入字符串时在字符串末尾附加一个'\0'二进制文件,对吧?
  • 字符串编码(UTF-8 等)是另一个问题;保存到文件时,你真的不需要担心,只有在例如显示或修剪字符串。这里我的意思是“编码”,序列化字符串对象。 — 是的,你也可以附加一个'\0',而不是先写长度,但那是 IME 相当麻烦(虽然很常见,毕竟这是 C 处理不知道编译时长度的字符串的标准方式)。长度编码当然会更浪费内存,但如果字符串足够长,则不会如此。
【解决方案2】:

仅仅写下内存中的二进制图像是一种序列化形式,对于琐碎的情况,它可以工作。但是一般来说,你需要解决一些仅仅转储内存没有考虑的问题:

1。指针

如果数据包含任何指针,当然你不能只是稍后转储负载,因为一旦程序终止并重新启动,指针指向的内存地址将毫无意义。许多对象都有“隐藏”指针...例如,无法将std::vector 转储到内存中并稍后正确重新加载...sizeof 上的std::vector 显然不包括包含元素的大小和因此,任何包含std::vector 的结构都不能只是转储和重新加载。 std::string 和所有其他 std 容器也是如此。

2。便携性

C 和 C++ 结构和类不是根据它们在内存中占用的字节来定义的,即不可移植。这意味着不同的编译器、不同的编译器版本甚至相同版本但具有不同编译选项的代码可能会生成内存中结构布局不同的代码。

如果您需要序列化来保存和重新加载同一程序中的数据,并且该数据不应该存在很长时间,那么确实可以使用内存转储。但是,只需考虑通过转储结构保存数百万个文档,现在新的编译器版本(您强制使用它,因为它是新操作系统版本唯一支持的)具有不同的布局和你不能再加载那些文件了。

除了相同系统的可移植性问题外,还要注意即使是单个整数也可以在不同系统上具有不同的内存表示。它可能更大或更小;它可能有不同的字节顺序。仅使用内存转储意味着保存的内容不能被另一个系统加载。甚至没有一个整数。

3。版本控制

如果您保存的数据具有较长的生命周期,那么您很可能会随着程序的发展而更改结构,例如您将添加新字段、删除未使用的字段、更改一般结构 (例如将向量更改为链表)。

如果您的格式只是当前数据结构的内存映像,那么以后很难将例如color 字段添加到polygon 对象并让程序可以加载旧文档,假设为默认颜色值是以前版本中使用的颜色。

即使编写转换程序也会很困难,因为您将拥有能够加载旧文档的旧代码和能够保存新文档的新代码,但您不能只是“合并”两者并获得一个加载旧文件并保存新文件的程序(即两个程序的源代码都将具有polygon 结构,但具有不同的字段,现在怎么办?)。

【讨论】:

  • 感谢您的回答,这让我明白为什么序列化是最重要的,:-)。但是这里如何使用序列化呢?
  • 我想知道,正如你所说,structure layout may vary with different compiler/platform,那么序列化如何处理这个问题?
【解决方案3】:

你在玩游戏。在非常困难的模式下。你到达最后一层。你很高兴。 2 天的不间断游戏正在获得回报。剧情很快就要结束了。你会发现邪恶策划者的动机,你是如何成为英雄的,并将收集在最后一扇门后面等待的广受欢迎的史诗神器。 而且您无需重新启动一次即可到达这里。

在幕后,有一个游戏对象,如下所示:

class GameState
{
   int level;
}

而且等级是25

到目前为止,你真的很喜欢这个游戏,但你不想从头再来,以防最后一个老板杀了你。因此,直观地,您按Ctrl+S。但是等等,你会得到一个错误:

Sorry, saving is disabled.

什么?所以我必须重新开始,以防我死?这怎么可能。

鼓声

开发人员虽然很出色(他们设法让您连续 2 天着迷,对吧?)并没有实现 序列化

当您重新启动游戏时,会进行内存清理。那个最重要的 GameState 对象,你花了 2 天时间将 level 成员增加到 25 的对象,被销毁了。

你怎么能解决这个问题?关闭游戏时,操作系统会回收内存。你能把它存放在哪里?在外部服务器上? (套接字)在磁盘上? (写入文件)

好吧,为什么不呢。

class GameState
{
    int level;
    void save(const std::string& fileName)
    { /* write level to file */ }
    void load(const std::string& fileName)
    { /* read game state from file */ }
};

当您按下Ctrl+s 时,GameState 对象将保存到文件中。

而且,神奇的是,当您加载游戏时,会从该文件中读取 GameState 对象。您不再需要花费 2 天时间才能回到上一个老板。你已经在那里了。

真正的答案:

从技术上讲,编写序列化功能非常困难。我建议你使用第三方。 Google 协议缓冲区提供了跨平台甚至跨语言的序列化。还有很多其他的。

1.那么序列化的意义何在?我这里需要吗?如果是,我应该如何使用它?

如上所述,它在运行之间或进程之间(可能在不同的机器上)存储状态。是否需要取决于您是否需要存储状态并稍后重新加载。

2.我一直认为任何类型的文件,.txt/.csv/.exe/...,在内存中都是01的位,这意味着它们自然具有二进制表示,所以我们不能简单地发送这些直接通过套接字文件?

他们是。但是您不想在玩新游戏时修改.exe

【讨论】:

  • 那么这个protocol buffer是做什么的,又是怎么做的呢?能否请您给我一个简单的纲要?
  • 我想知道protocol buffer是如何处理这些问题的,比如不同平台对int/string的表示不同等等。
  • @Alcott 它是开源的,请随意查看 :)
【解决方案4】:

您的字符串将无法正确保存。如果您有不同的机器,它们对整数的表示可能会有所不同,例如,不同的编程语言不会对字符串有相同的表示。

但是当您有指向成员的指针时,您将保存指针地址而不是指向的成员,这意味着您无法再次从文件中获取该数据。如果你的结构需要改变怎么办?所有使用您的数据的软件都需要更改。

是的,您可以通过套接字发送文件,但您需要某种协议以确保您知道文件的名称以及何时到达文件末尾。

【讨论】:

    【解决方案5】:

    序列化做了很多事情。它支持持久性(能够 离开程序,然后返回程序并获取相同的数据), 以及进程和机器之间的通信。这基本上意味着 将您的内部数据转换为字节序列,并且有用, 您还必须支持反序列化:转换序列 字节返回数据。

    当您这样做时,重要的是要在内部意识到这一点 程序,数据不仅仅是一个字节序列。它有格式和 结构:double 的表示方式与一台机器不同 到下一个,例如;还有更复杂的对象,比如std::string, 甚至不在连续的内存中。所以你要做的第一件事 序列化时定义每种类型如何表示为序列 字节数。如果您正在与另一个程序通信,则两个程序 必须同意这种串行格式;如果只是为了让你可以重读 数据自己,你可以使用任何你想要的格式(但我建议 使用预定义的标准格式,如 XDR,如果只是为了简化 文档)。

    你不能做的只是在内存中转储对象的图像。 像std::string 这样的复杂对象将在其中包含指针,而这些 指针在另一个进程中将毫无意义。甚至 像double 这样的简单类型的表示可能会随着时间而改变。 (这 从 32 位迁移到 64 位导致 long 的大小发生变化 在大多数系统上。)您必须定义一个格式,然后生成它字节 从您拥有的数据中按字节计算。例如,要编写 XDR,您可以 使用这样的东西:

    typedef std::vector<char> Buffer;
    
    void
    writeUInt( Buffer& dest, unsigned value )
    {
        dest.push_back( (value >> 24) & 0xFF );
        dest.push_back( (value >> 16) & 0xFF );
        dest.push_back( (value >>  8) & 0xFF );
        dest.push_back( (value      ) & 0xFF );
    }
    
    void
    writeInt( Buffer& dest, int value )
    {
        writeUInt( dest, static_cast<unsigned>( value ) );
    }
    
    void
    writeString( Buffer& dest, std::string const& value)
    {
        assert( value.size() <= 0xFFFFFFFF );
        writeInt( dest, value.size() )
        std::copy( value.begin(), value.end(), std::back_inserter( dest ) );
        while ( dest.size() % 4 != 0 ) {
            dest.push_back( '\0' );
        }
    }
    

    【讨论】:

      【解决方案6】:

      除了大 edian 或 little endian 之外,还有一个问题是如何使用该编译器为该程序的给定结构打包数据。如果要保存整个结构,则不能使用任何指针,则必须将其替换为足够大的字符缓冲区以满足您的需要。如果另一台机器将采用相同的架构,那么如果您使用#pragma pack(1),您的结构字段之间将不会有任何间隙,并且您可以确保数据看起来好像是序列化的,但没有字符串的大小前缀。如果您确定将读取数据的其他程序对于相同的结构具有完全相同的设置,则可以跳过#pragma pack(1)。除此之外,数据将不匹配。

      如果先序列化到内存,可以加快序列化过程。对于大多数类型,这通常可以通过一个缓冲区类和一个模板函数来完成。

      template<typename T>
      buffer& operator<<(T data)
      {
          *(T*)buf = data;
          buf += sizeof(T);
      }
      

      显然,您需要专门的字符串和更大的数据类型。您可以将 memcpy 用于大型结构并传入指向数据的指针。对于字符串,您需要为前面提到的长度添加前缀。

      不过,对于严重的序列化需求,还有很多需要考虑的因素。

      【讨论】:

        猜你喜欢
        • 2012-08-02
        • 1970-01-01
        • 2011-01-30
        • 1970-01-01
        • 2011-10-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多