恢复通过网络发送的结构
Recovering structs sent over a network
我的同事想通过网络发送一些由 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<sizeof(T), alignof(T)>
来接收这样的结构怎么样?如果 std::aligned_storage
也不合法,是否有 any 通过网络发送 raw 结构的合法方式,或者它是一个坏的恰好可行的想法......直到它不起作用?
我将结构视为表示数据类型的一种方式,并将编译器在内存中布局它们的方式视为实现细节,而不是作为数据交换所依赖的有线格式,但我愿意接受错了。
[1] www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf
危险的部分与其说是内存对齐,不如说是 T
对象的生命周期。当您 reinterpret_cast<>
内存作为 T
指针时,不会创建对象的实例,并且像使用它一样会导致 Undefined B行为。
在C++中,所有对象都必须产生和停止存在,从而定义它们的生命周期。这甚至适用于 int
和 float
等基本数据类型。唯一的例外是 char
.
换句话说,合法的做法是将缓冲区中的字节复制到一个已经存在的对象中,如下所示:
char buffer[sizeof(T)];
// fill the buffer...
T value;
std::memcpy(&value, buffer, sizeof(T));
不用担心性能问题。编译器将优化所有这些。
我的同事想通过网络发送一些由 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<sizeof(T), alignof(T)>
来接收这样的结构怎么样?如果 std::aligned_storage
也不合法,是否有 any 通过网络发送 raw 结构的合法方式,或者它是一个坏的恰好可行的想法......直到它不起作用?
我将结构视为表示数据类型的一种方式,并将编译器在内存中布局它们的方式视为实现细节,而不是作为数据交换所依赖的有线格式,但我愿意接受错了。
[1] www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf
危险的部分与其说是内存对齐,不如说是 T
对象的生命周期。当您 reinterpret_cast<>
内存作为 T
指针时,不会创建对象的实例,并且像使用它一样会导致 Undefined B行为。
在C++中,所有对象都必须产生和停止存在,从而定义它们的生命周期。这甚至适用于 int
和 float
等基本数据类型。唯一的例外是 char
.
换句话说,合法的做法是将缓冲区中的字节复制到一个已经存在的对象中,如下所示:
char buffer[sizeof(T)];
// fill the buffer...
T value;
std::memcpy(&value, buffer, sizeof(T));
不用担心性能问题。编译器将优化所有这些。