【问题标题】:C++ serialization - use of reinterpret_cast from char * to a structC++ 序列化 - 使用从 char * 到结构的 reinterpret_cast
【发布时间】:2015-01-19 06:23:46
【问题描述】:

我正在使用sendto(..)recvfrom() 与通过UDP 套接字运行相同程序的其他服务器(相同或类似系统的)交换一个名为struct update_packet 的结构。

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()ing 函数吗?

【问题讨论】:

  • 在 C++ 中,struct 类型可以独立引用,不需要struct 限定符,即您可以简单地编写update_packet 而不是struct update_packet

标签: c++ sockets serialization deserialization


【解决方案1】:

仅谈到解码(您的计算机 - 您的规则),在 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());

【讨论】:

    【解决方案2】:

    一般性

    演员问题已在其他问题中得到正确回答。

    但是,您永远不应该依赖指针转换来通过网络发送/接收结构,原因有很多,包括:

    • 打包:编译器可以对齐结构变量并插入填充字节。这取决于编译器,因此您的代码将不可移植。如果两台通信机器运行您使用不同编译器编译的程序,它可能无法正常工作。
    • 字节序:同理,两台机器发送多字节数(如 int)时的字节顺序可能不同。

    这将导致代码可能工作一段时间,但几年后会导致很多问题,如果有人更改编译器、平台等......因为这是一个教育项目您应该尝试以正确的方式进行操作...

    因此,将数据从 struct 转换为 char 数组以通过网络发送或写入文件时应谨慎进行,逐个变量进行,并在可能的情况下考虑字节序。这个过程称为“序列化”。

    序列化详解

    序列化意味着您将数据结构转换为字节数组,可以通过网络发送。

    序列化格式不一定是二进制:文本或 xml 是可能的选项。如果数据量很小,文本可能是最好的解决方案,并且您可以仅依靠带有字符串流的 STL(std::istringstream 和 std::ostringstream)

    有几个很好的库可以序列化为二进制,例如 Qt 中的 Boost::serialization 或 QDataStream。 你也可以自己做,寻找“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 &gt;&gt; up.num_update_fields; 之后的节点数,如果它太大,则在分配一个巨大的缓冲区之前中止解码,这会导致您的程序甚至系统崩溃。网络攻击是基于这样的“漏洞”:如果不进行这种检查,您可能会通过让服务器分配比其 RAM 大 100 倍的缓冲区来导致服务器崩溃。
    • 如果您的网络 API 具有 std::iostream 接口,您可以直接使用其中的 > 运算符,而无需使用中间字符串和字符串流
    • 您可能认为使用空格分隔的文本会浪费带宽。仅当您的节点数量很大时才考虑这一点,并使带宽使用变得不可忽略且至关重要。在这种情况下,您需要序列化为二进制。但是,如果文本解决方案完美运行,请不要这样做(注意过早优化!)

    简单的二进制序列化(不支持字节顺序/字节序):

    替换:

    oss.write << up.port;
    

    作者:

    oss.write((const char *)&up.port, sizeof(up.port));
    

    字节序

    但在您的项目中,Big-Endian 是必需的。如果您在 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之类的库

    这些库允许您选择输出数据的字节顺序。然后他们会自动检测平台字节序并自动完成这项工作。

    【讨论】:

    • 这个项目的一个要求是更新包的大小是固定的(32*2 位用于标头,32*3 用于每个node)。 XML 显然为此添加了更多位,因此我无法使用它。另外,在这种情况下,我猜序列化的输出应该是具有相同大小的二进制文件。我想知道这是怎么做到的。例如,具有十进制值的 16 位整数,例如 500,在 uint16_t 中是 00000001 00101100。当 1 个字符占用 8 位时,如何将其转换为占用 16 位的字符串?
    • 好吧。没关系。我想我还是不清楚项目规范
    • 顺便问一下struct node中的节点数组该怎么办?数组是指向地址的指针,向另一台机器发送指针是没有意义的,所以我应该在该数组中创建一个包含nodes 数据的 char *,并且序列化是这样做的方法吗?
    • 是的。最简单/最安全的方法可能是使用 ostringstream 和
    • 如果我不能使用空格分隔符,那么在检索数据时使用数据类型的大小是否安全? (这会很复杂..有什么技巧可以简单地做到这一点吗?)num_update_fields字段实际上是struct nodes的数量,所以也许我可以用它来检索struct nodes的序列化数据。
    【解决方案3】:

    你也可以使用:

    struct update_packet update_msg;
    
    memcpy(&update_msg, recved_msg, size-of-message);
    

    但是,您必须确保 size-of-message 正是您要查找的内容。

    【讨论】:

    • 这是 the 方法来做这样的事情,因为它确保(必要的琐碎可复制的)目标正确对齐和活动 - 不像经常引用的,可怕的未定义尝试reinterpret_cast 一些char *就对象模型而言不存在的未对齐假想对象。另外,如果本机平台和编译器支持它,as-if 规则意味着他们可以完全合法地优化 struct 实例并将其访问到相同的 hacky、可能未对齐的转换 无论如何 - 假设这样做会带来足够的物质利益
    【解决方案4】:

    您不能将指针强制转换为结构,但可以将指针强制转换为指向结构的指针。

    改变

    struct update_packet update_msg = 
           reinterpret_cast<struct update_packet>(recved_msg);
    

    update_packet * update_msg = 
           reinterpret_cast<update_packet *>(recved_msg);
    

    是的,您至少需要pack(),因为发送端的编译器可能会以不同的方式添加填充。但是,它不是 100% 安全的。您还考虑到发送和接收机器的字节序不同。我建议您研究适当的序列化机制。

    【讨论】:

    • 打包依赖于编译器,不可移植。依赖它进行序列化是懒惰的,尽管需要更多代码,但可以正确序列化为 char 数组,提供更好的控制,解决字节顺序问题,并且更便携和更安全。
    • 可移植性不是问题,因为它只是一个学校项目,所有字段都需要大端。您的解决方案适用于 update_packet 中的 int 字段,但我可以再问一个问题吗?我在update_packet 中有一个struct node 的指针数组。我试图通过 update_msg->nodes[0]->cost、update_msg-&gt;nodes-&gt;cost 等引用此数组 nodes 的一个元素,但是它们失败了。我认为在消息中包含指针数组可能会出现问题,但是您知道究竟是什么问题吗?
    • 好吧,我会说相反...如果是为了教育,请学习干净的方法来做可靠的事情。如果是用于太空发射,请使用干净可靠的代码。如果是卫生纸厂工程开发,使用干净可靠的代码,避免10年后OS发生变化,有人调试你的代码。
    • 对于一个学校项目,如果你会在代码上得到注释,那么你会在最先进的序列化中得到更好的注释(特别是如果其他学生用蹩脚的方式)
    • 已编辑:这不是未定义的行为,在这种情况下你很幸运!但我敢打赌你没有检查这个(见类型别名规则部分)en.cppreference.com/w/cpp/language/reinterpret_cast
    猜你喜欢
    • 2012-11-16
    • 2013-03-25
    • 2010-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-21
    • 2011-04-25
    相关资源
    最近更新 更多