【问题标题】:Recovering structs sent over a network恢复通过网络发送的结构
【发布时间】:2021-05-31 23:51:21
【问题描述】:

我的同事想通过网络发送一些由 T 类型表示的数据。他通过将T 转换为char* 并使用带有套接字的write(2) 调用发送它来执行此传统方式™:

auto send_some_t(int sock, T const* p) -> void
{
    auto buffer = reinterpret_cast<char const*>(p);
    write(sock, buffer, sizeof(T));
}

到目前为止,一切都很好。这个简化的例子,除了去掉任何错误检查,应该是正确的。假设 T 类型可以简单地复制,我们可以使用 std::mempcy() 在对象之间复制这种类型的值(根据 C++17 标准 [1] 中的 6.7 [basic.types] 第 3 点),所以我猜 write(2) 也应该可以工作因为它盲目地复制二进制数据。

棘手的地方在于接收方。

假设有问题的类型 T 如下所示:

struct T {
    uint64_t foo;
    uint8_t bar;
    uint16_t baz;
};

它有一个需要 8 个字节对齐的字段 (foo),因此整个类型需要 8 个字节的严格对齐(参见 6.6.5 [basic.align] 第 2 点的示例)。这意味着T 类型的值的存储必须只分配在合适的地址上。

现在,下面的代码呢?

auto receive_some_t(int sock, T* p) -> void
{
    read(sock, p, sizeof(T));
}

// ...

T value;
receive_some_t(sock, &T);

看起来很阴暗,但应该可以正常工作。接收到的字节do 代表T 类型的有效值,并被盲目复制到T 类型的有效对象中。

但是,如何使用原始 char 缓冲区,如以下代码:

char buffer[sizeof(T)];
read_some_t(sock, buffer);

T* value = reinterpret_cast<T*>(buffer);

这是我的程序员大脑触发红色警报的地方。我们绝对不能保证char[sizeof(T)] 的对齐方式与T 的对齐方式相匹配,这是一个问题。我们也不会往返指向有效T 对象的指针,因为在我们的内存中没有 类型为T 的有效对象。而且我们不知道另一边使用了哪些编译器和选项(也许另一边的结构被打包了,而我们的没有)。

简而言之,我发现将原始char 缓冲区转换为其他类型存在一些潜在问题,并且会尽量避免编写上述代码。但显然它是有效的,并且是“每个人都这样做”的方式。

我的问题是:根据 C++17 标准,恢复通过网络发送并接收到具有适当大小的 char 缓冲区的结构是否合法?

如果不是,那么使用std::aligned_storage&lt;sizeof(T), alignof(T)&gt; 接收这样的结构呢?如果std::aligned_storage 也不合法,是否有任何 合法的方式通过网络发送raw 结构,或者碰巧工作是个坏主意.. . 直到没有?

我将结构体视为一种表示数据类型的方式,并将编译器在内存中布置它们的方式视为实现细节,而不是作为数据交换所依赖的有线格式,但我愿意犯错。

[1]www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf

【问题讨论】:

  • 但显然它有效,并且“每个人都这样做”。并不总是有效,有时每个人都错了。
  • 上面显示的任何东西都不能保证工作。对于网络套接字,无论如何,都不能保证read() 将从其第三个参数指定的套接字中读取字节数。假设套接字保持打开状态,它可能介于 1 和该值之间。忽略read()的返回值总是会以泪水收场。
  • 无法保证在一台机器上编译的T类型的字节模式与@987654352类型的字节模式相同@ 在另一台机器上编译。即使您使用相同的架构,不同版本的编译器理论上也可能会做不同的事情。你可能会侥幸逃脱,但它充其量是非常不便携的。
  • 在所有情况下,您都不应该将 reinterpret_cast 复制到您的类型,而是将字节从 char 缓冲区 复制到 T 类型的对象中.但我会推荐一些比通过网络发送原始二进制文件更便携的东西。
  • 不要这样做。不要将结构用作网络协议。使用网络协议作为网络协议。用八位字节定义它,并为自己编写一个库来发送和接收它。目前,您完全依赖于在两端具有相同的 (a) 编译器 (b) 编译器选项 (c) 字节序 (d) 对齐 (e) 填充。真正确保它必须工作的唯一方法是从相同的 .o 或 .obj 文件构建两端,用于发送和接收以及 struct 解释。

标签: c++ serialization struct c++17 data-exchange


【解决方案1】:

冒险的部分不是内存对齐,而是T 对象的生命周期。当您将reinterpret_cast&lt;&gt; 内存作为T 指针时,不会创建对象的实例,并且像使用它一样使用它会导致 Undefined B行为。

在 C++ 中,所有对象都必须生成并停止存在,从而定义它们的生命周期。这甚至适用于intfloat 等基本数据类型。唯一的例外是char

换句话说,合法的是将缓冲区中的字节复制到已经存在的对象中,如下所示:

char buffer[sizeof(T)];

// fill the buffer...

T value;
std::memcpy(&value, buffer, sizeof(T));

不用担心性能。编译器将优化所有这些。

【讨论】:

  • 你也可以read 直接进入结构,使用endian等常见的警告。
  • @user4581301 好吧,OP 已经将其作为问题的一部分。
  • 确实如此。在我的通读过程中不知何故错过了这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-10
相关资源
最近更新 更多