C++序列化 - 使用从char*到结构的reinterpret_cast

Eri*_* Na 3 c++ sockets serialization deserialization

我正在交换一个struct update_packet与其他服务器(相同或类似系统)调用的结构,通过UDPsocket使用sendto(..)和运行相同的程序recvfrom().

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};
};
Run Code Online (Sandbox Code Playgroud)

(update_packet包含一个指针数组struct node)

我以前用UDP reinterpret_cast发送一个实例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));
Run Code Online (Sandbox Code Playgroud)

但是,当我收到它并尝试解码它

struct update_packet update_msg =
    reinterpret_cast<struct update_packet>(recved_msg);
Run Code Online (Sandbox Code Playgroud)

我收到一个错误

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);
Run Code Online (Sandbox Code Playgroud)

为什么会出现此错误,我该如何解决?

另外,这是struct通过套接字实例交换数据的正确方法吗?如果没有,我该怎么办?我是否需要http://beej.us/guide/bgnet/examples/pack2.c中pack()ing功能?

gal*_*tte 5

泛泛而谈

在其他问题中已经正确回答了演员问题.

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

  • 打包:编译器可以对齐struct变量并插入填充字节.这取决于编译器,因此您的代码将不可移植.如果两个通信机器运行用不同编译器编译的程序,它可能不起作用.
  • 字节顺序:出于同样的原因,发送多字节数字(例如int)时的字节顺序在两台机器之间可能不同.

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

出于这个原因,将结构中的数据转换为char数组以便通过网络发送或写入文件应该仔细地进行,逐个变量,并且如果可能的话考虑到endianness.此过程称为"序列化".

序列化细节

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

序列化格式不一定是二进制格式:text或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();
Run Code Online (Sandbox Code Playgroud)

并用于反序列化收到的数据:

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;
}
Run Code Online (Sandbox Code Playgroud)

这将是100%便携和安全.您可以通过检查iss错误标志来验证数据有效性.

你也可以为了安全起见:

  • 使用std :: vector代替节点指针.这样可以防止内存泄漏等问题
  • 检查刚刚之后的节点数iss >> up.num_update_fields;,如果它太大只是在分配一个巨大的缓冲区之前中止解码,这将导致程序崩溃,也许系统崩溃.网络攻击基于这样的"漏洞":如果不进行此类检查,您可能会使服务器崩溃,使其分配比RAM大100倍的缓冲区.
  • 如果您的网络API有一个std :: iostream接口,您可以直接使用它的<<和>>运算符,而不使用中间字符串和字符串流
  • 您可能认为使用空格分隔文本会浪费带宽.只有在节点数量很大的情况下才能想到这一点,并使带宽使用变得不可忽视且至关重要.在这种情况下,您需要序列化为二进制.但如果文本解决方案完美运行,请不要这样做(谨防过早优化!)

简单的二进制序列化(不是字节顺序/字节顺序感知):

更换:

oss.write << up.port;
Run Code Online (Sandbox Code Playgroud)

通过:

oss.write((const char *)&up.port, sizeof(up.port));
Run Code Online (Sandbox Code Playgroud)

字节序

但在您的项目中,需要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]);
Run Code Online (Sandbox Code Playgroud)

终极代码:检测字节序(这不难做到 - 在SO上查找)并调整序列化代码.

2)第二个选项:使用像boost或Qt这样的库

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