我有一个小的对象层次结构,我需要通过套接字连接对其进行序列化和传输。我需要序列化对象,然后根据它的类型反序列化它。有没有一种简单的方法可以在 C++ 中做到这一点(就像在 Java 中一样)?
为了清楚起见,我正在寻找将对象转换为字节数组,然后再转换回对象的方法。我可以处理套接字传输。
原文由 Bill the Lizard 发布,翻译遵循 CC BY-SA 4.0 许可协议
我有一个小的对象层次结构,我需要通过套接字连接对其进行序列化和传输。我需要序列化对象,然后根据它的类型反序列化它。有没有一种简单的方法可以在 C++ 中做到这一点(就像在 Java 中一样)?
为了清楚起见,我正在寻找将对象转换为字节数组,然后再转换回对象的方法。我可以处理套接字传输。
原文由 Bill the Lizard 发布,翻译遵循 CC BY-SA 4.0 许可协议
有一个通用模式可用于序列化对象。基本原语是您可以从迭代器中读取和写入的这两个函数:
template <class OutputCharIterator>
void putByte(char byte, OutputCharIterator &&it)
{
*it = byte;
++it;
}
template <class InputCharIterator>
char getByte(InputCharIterator &&it, InputCharIterator &&end)
{
if (it == end)
{
throw std::runtime_error{"Unexpected end of stream."};
}
char byte = *it;
++it;
return byte;
}
然后序列化和反序列化函数遵循以下模式:
template <class OutputCharIterator>
void serialize(const YourType &obj, OutputCharIterator &&it)
{
// Call putbyte or other serialize overloads.
}
template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
// Call getByte or other deserialize overloads.
}
对于类,您可以使用友元函数模式来允许使用 ADL 找到重载:
class Foo
{
int internal1, internal2;
// So it can be found using ADL and it accesses private parts.
template <class OutputCharIterator>
friend void serialize(const Foo &obj, OutputCharIterator &&it)
{
// Call putByte or other serialize overloads.
}
// Deserialize similar.
};
然后在你的程序中,你可以序列化和对象到这样的文件中:
std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));
然后阅读:
std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());
我的旧答案在这里:
序列化意味着将您的对象转换为二进制数据。而反序列化意味着从数据中重新创建一个对象。
序列化时,您将字节推入 uint8_t
向量。反序列化时,您正在从 uint8_t
向量中读取字节。
在序列化内容时,您当然可以使用一些模式。
每个可序列化类都应该有一个 serialize(std::vector<uint8_t> &binaryData)
或类似的签名函数,它将其二进制表示写入提供的向量。然后这个函数可以把这个向量传递给它的成员的序列化函数,这样他们也可以把他们的东西写进去。
因为数据表示在不同的架构上可能不同。您需要找出如何表示数据的方案。
让我们从基础开始:
只需按小端顺序写入字节即可。如果大小很重要,或者使用 varint 表示。
以小端顺序序列化:
data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);
从小端顺序反序列化:
integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
据我所知,IEEE 754 在这里拥有垄断地位。我不知道任何主流架构会使用其他东西作为浮动。唯一可以不同的是字节顺序。一些体系结构使用小端,另一些使用大端字节顺序。这意味着您需要小心在接收端大声播放字节的顺序。另一个区别可能是处理非正规和无穷大和 NAN 值。但只要你避免这些值,你应该没问题。
序列化:
uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...
反序列化是向后做的。注意架构的字节顺序!
首先,您需要就编码达成一致。 UTF-8 很常见。然后将其存储为长度前缀的方式:首先使用我上面提到的方法存储字符串的长度,然后逐字节写入字符串。
它们与字符串相同。您首先序列化一个表示数组大小的整数,然后序列化其中的每个对象。
正如我之前所说,他们应该有一个 serialize
向向量添加内容的方法。要反序列化一个对象,它应该有一个接受字节流的构造函数。它可以是 istream
但在最简单的情况下,它可以只是一个引用 uint8_t
指针。构造函数从流中读取它想要的字节并设置对象中的字段。如果系统设计良好并按对象字段顺序序列化字段,您只需将流传递给初始化列表中的字段构造函数,并以正确的顺序对它们进行反序列化。
首先,您需要确定这些对象是否真的是您想要序列化的东西。如果目标上存在这些对象的实例,则不需要序列化它们。
现在您发现需要序列化指针指向的对象。仅在使用它们的程序中才有效的指针问题。您不能序列化指针,您应该停止在对象中使用它们。而是创建对象池。这个对象池基本上是一个包含“盒子”的动态数组。这些框有一个引用计数。非零引用计数表示一个活动对象,零表示一个空槽。然后创建类似于 shared_ptr 的智能指针,它不存储指向对象的指针,而是存储数组中的索引。您还需要就表示空指针的索引达成一致,例如。 -1。
基本上我们在这里所做的是将指针替换为数组索引。现在在序列化时,您可以像往常一样序列化这个数组索引。您无需担心对象将在目标系统的内存中的什么位置。只要确保它们也具有相同的对象池。
所以我们需要序列化对象池。但哪些?好吧,当您序列化一个对象图时,您不仅仅是在序列化一个对象,而是在序列化整个系统。这意味着系统的序列化不应从系统的某些部分开始。这些对象不应该担心系统的其余部分,它们只需要序列化数组索引就可以了。您应该有一个系统序列化程序来协调系统的序列化并遍历相关的对象池并序列化所有对象池。
在接收端,所有数组和其中的对象都被反序列化,重新创建所需的对象图。
不要在对象中存储指针。有一个静态数组,其中包含指向这些函数的指针并将索引存储在对象中。
由于这两个程序都将该表编译到它们的架子中,因此仅使用索引应该可以工作。
既然我说你应该避免使用可序列化类型的指针,而应该使用数组索引,多态性就无法工作,因为它需要指针。
您需要使用类型标签和联合来解决这个问题。
在所有上述之上。您可能希望不同版本的软件互操作。
在这种情况下,每个对象都应该在其序列化的开头写一个版本号来指示版本。
在另一侧加载对象时,较新的对象可能能够处理较旧的表示,但较旧的对象无法处理较新的表示,因此它们应该对此抛出异常。
每次发生变化时,您都应该增加版本号。
所以总结一下,序列化可能很复杂。但幸运的是,您不需要序列化程序中的所有内容,通常只序列化协议消息,这些消息通常是普通的旧结构。所以你不需要我上面经常提到的复杂技巧。
原文由 Calmarius 发布,翻译遵循 CC BY-SA 4.0 许可协议
3 回答2k 阅读✓ 已解决
2 回答3.9k 阅读✓ 已解决
2 回答3.2k 阅读✓ 已解决
1 回答3.2k 阅读✓ 已解决
1 回答2.7k 阅读✓ 已解决
3 回答3.4k 阅读
1 回答1.6k 阅读✓ 已解决
说到序列化,我想到了 boost 序列化 API 。至于通过网络传输序列化数据,我会使用 Berkeley 套接字或 asio 库。
如果要将对象序列化为字节数组,可以按以下方式使用 boost 序列化程序(取自教程站点):
实际的序列化非常简单:
反序列化以类似的方式工作。
还有一些机制可以让您处理指针的序列化(复杂的数据结构,如树等没有问题),派生类,您可以在二进制和文本序列化之间进行选择。此外,所有 STL 容器都支持开箱即用。