C++序列化 - 使用从 char * 到结构的reinterpret_cast
C++ serialization - use of reinterpret_cast from char * to a struct
我正在使用sendto(..)
和recvfrom()
通过套接字与其他服务器(相同或类似系统)交换名为struct update_packet
的结构UDP
运行相同的程序。
update_packet
需要采用通用消息格式,这意味着其字段具有预定的固定大小,结构的大小是字段的总和。
struct node {
uint32_t IP;
uint16_t port;
int16_t nil;
uint16_t server_id;
uint16_t cost;
};
struct update_packet {
uint16_t num_update_fields;
uint16_t port;
uint32_t IP;
struct node * nodes;
update_packet() :
num_update_fields(num_nodes), IP(myIP), port(myport)
{//fill in nodes array};
};
( update_packet
包含一个指针数组 struct node
)
我使用 reinterpret_cast
通过 UDP 发送update packet
实例,以下内容编译并发送到正确的目的地。
int update_packet_size = sizeof(up);
sendto(s, reinterpret_cast<const char*>(&up), update_packet_size, 0,
(struct sockaddr *)&dest_addr, sizeof(dest_addr));
但是,当我收到它并尝试通过以下方式解码它时
struct update_packet update_msg =
reinterpret_cast<struct update_packet>(recved_msg);
我收到错误
In function ‘int main(int, char**)’:
error: invalid cast from type ‘char*’ to type ‘update_packet’
struct update_packet update_msg =
reinterpret_cast<struct update_packet>(recved_msg);
为什么会发生此错误,我该如何解决此问题?
另外,这是通过套接字在struct
实例中交换数据的正确方法吗?如果没有,我该怎么办?我需要像 http://beej.us/guide/bgnet/examples/pack2.c 那样的pack()
功能吗?
一般情况
演员表问题在其他问题中得到了适当的回答。
但是,切勿依赖指针强制转换通过网络发送/接收结构,原因有很多,包括:
- 打包:编译器可以对齐结构变量并插入填充字节。这依赖于编译器,因此您的代码将不可移植。如果两台通信机器运行使用不同编译器编译的程序,则可能无法正常工作。 字节
- 序 :出于同样的原因,发送多字节数字(例如 int)时的字节顺序在两台机器之间可能不同。
这将导致代码可能会工作一段时间,但几年后,如果有人更改编译器、平台等,这将导致很多问题......由于这是一个教育项目,您应该尝试以正确的方式进行......
因此,将数据从结构转换为 char 数组以通过网络发送或写入文件时,应逐个变量仔细完成,如果可能的话,应考虑字节序。此过程称为"序列化"。
序列化详解
序列化意味着将数据结构转换为可通过网络发送的字节数组。
序列化格式不一定是二进制的:文本或xml是可能的选项。如果数据量很小,文本可能是最好的解决方案,你可以只依赖字符串流的STL(std::istringstream和std::ostringstream)。
有几个很好的库可以序列化为二进制,例如Qt中的Boost::serialization或QDataStream。你也可以自己做,寻找SO的"C++序列化"
使用 STL 轻松序列化为文本
在您的情况下,您可能只是使用以下内容序列化为文本字符串:
std::ostringstream oss;
oss << up.port;
oss << up.IP;
oss << up.num_update_fields;
for(unsigned int i=0;i<up.num_update_fields;i++)
{
oss << up.nodes[i].IP;
oss << up.nodes[i].port;
oss << up.nodes[i].nil;
oss << up.nodes[i].server_id;
oss << up.nodes[i].cost;
}
std::string str = oss.str();
char * data_to_send = str.data();
unsigned int num_bytes_to_send = str.size();
对于反序列化接收的数据:
std::string str(data_received, num_bytes_received);
std::istringstream(str);
update_packet up;
iss >> up.port;
iss >> up.IP;
iss >> up.num_update_fields;
//maximum number of nodes should be checked here before doing memory allocation!
up.nodes = (nodes*)malloc(sizeof(node)*up.num_update_fields);
for(unsigned int i=0;i<up.num_update_fields;i++)
{
iss >> up.nodes[i].IP;
iss >> up.nodes[i].port;
iss >> up.nodes[i].nil;
iss >> up.nodes[i].server_id;
iss >> up.nodes[i].cost;
}
这将是100%便携和安全的。您可以通过检查 iss 错误标志来验证数据的有效性。
为了安全起见,您也可以:
- 使用 std::vector 而不是节点指针。这将防止内存泄漏和其他问题
iss >> up.num_update_fields;
后检查节点数,如果太大,只需在分配一个巨大的缓冲区之前中止解码,这将使您的程序崩溃,甚至会使系统崩溃。网络攻击基于这样的"漏洞":如果不进行这种检查,您可能会通过让他分配比其 RAM 大 100 倍的缓冲区来导致服务器崩溃。- 如果您的网络 API 具有 std::iostream 接口,则可以直接使用其中的<<和>>运算符,而无需使用中间字符串和字符串流
- 您可能认为使用空格分隔的文本是浪费带宽。仅当您的节点数量很大,并且使带宽使用变得不可忽视和关键时,才考虑这一点。在这种情况下,您需要序列化为二进制。但是,如果文本解决方案运行良好,请不要这样做(当心过早优化!
简单二进制序列化(不识别字节顺序/字节序):
取代:
oss.write << up.port;
由:
oss.write((const char *)&up.port, sizeof(up.port));
字节序
但是在您的项目中,需要大端序。如果您在 PC (x86) 上运行,则需要在每个字段中反转字节。
1)第一种选择:手工
const char * ptr = &up.port;
unsigned int s = sizeof(up.port);
for(unsigned int i=0; i<s; i++)
oss.put(ptr[s-1-i]);
终极代码 :检测字节序(这并不难做到 - 在 SO 上查找它)并调整您的序列化代码。
2)第二种选择:使用像boost或Qt这样的库
这些库允许您选择输出数据的字节序。然后,它们会自动检测平台字节序并自动完成工作。
不能强制转换为结构的指针,但可以将指针强制转换为指向结构的指针。
改变
struct update_packet update_msg =
reinterpret_cast<struct update_packet>(recved_msg);
自
update_packet * update_msg =
reinterpret_cast<update_packet *>(recved_msg);
是的,您需要至少pack()
因为发送端的编译器可能会以不同的方式添加填充。但是,它不是100%安全的。您还必须考虑到发送和接收机器的字节序不同。我建议您研究适当的序列化机制。
您也可以使用:
struct update_packet update_msg;
memcpy(&update_msg, recved_msg, size-of-message);
但是,您必须确保size-of-message
正是您要查找的。
解码(您的计算机 - 您的规则),在 GCC 和 Clang 上都可以考虑字节序和打包,并带有这样的组合(它使用 Boost.Endian 库):
#include <boost/endian/arithmetic.hpp>
using boost::endian::big_uint16_t;
using boost::endian::big_uint32_t;
using boost::endian::big_uint64_t;
#pragma pack(push, 1)
enum class e_message_type: uint8_t {
hello = 'H',
goodbye = 'G'
};
struct message_header {
big_uint16_t size;
e_message_type message_type;
std::byte reserved;
};
static_assert(sizeof(header) == 4);
struct price_quote {
big_uint64_t price;
big_uint32_t size;
big_uint32_t timestamp;
};
static_assert(sizeof(header) == 16);
template<class T> struct envelope {
message_header header;
T payload;
};
static_assert(sizeof(envelope<price_quote>) == 20);
#pragma pack(pop)
// and then
auto& x = *static_cast<envelope const*>(buffer.data());
- 如何循环打印顶点结构
- 通过方法访问结构
- 使用不带参数的函数访问结构元素
- 预处理器:插入结构名称中的前一个行号
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 孤立代码块在结构中引发异常
- 有什么方法可以遍历结构吗
- 如何在 C# 中映射双 C 结构指针?
- 如何在C++中使用结构生成映射
- 无法将结构注册为增强几何体3D点
- 多成员Constexpr结构初始化
- C++将文本文件中的数据读取到结构数组中
- 如何重构类层次结构以避免菱形问题
- 如何在C++中序列化结构数据
- std::vector的包装器,使数组的结构看起来像结构的数组
- 没有为自己的结构调用列表推回方法
- 奇怪的结构&GCC&clang(void*返回类型)
- 在 c++ 中拥有一组结构的正确方法是什么?
- vscode g++链路故障:体系结构x86_64的未定义符号
- C++概念:如何使用'concept'检查模板化结构的属性?