结构体的序列化

19
假设我有一个结构体,其中包含我想要使用winsock 2将其成员值发送到另一个系统的内容。 我正在使用C ++语言。 如何将其转换为char *,并考虑在发送之前必须对结构进行序列化,以及如何在另一端将char *反序列化为结构?我发现boost序列化被建议用于类似的问题,但是否可以用简短的代码片段说明序列化和反序列化的过程?
这个问题可能看起来很基础,但其他相关帖子中的答案并没有提供太多帮助。

1
Boost网站上的序列化示例怎么样? - nogard
5个回答

30

以下示例展示了将struct序列化为char数组并进行反序列化的最简单方法。

#include <iostream>
#include <cstring>

#define BUFSIZE 512
#define PACKETSIZE sizeof(MSG)

using namespace std;

typedef struct MSG
{
    int type;
    int priority;
    int sender;
    char message[BUFSIZE];
}MSG;

void serialize(MSG* msgPacket, char *data);
void deserialize(char *data, MSG* msgPacket);
void printMsg(MSG* msgPacket);

int main()
{
    MSG* newMsg = new MSG;
    newMsg->type = 1;
    newMsg->priority = 9;
    newMsg->sender = 2;
    strcpy(newMsg->message, "hello from server\0");
    printMsg(newMsg);

    char data[PACKETSIZE];

    serialize(newMsg, data);

    MSG* temp = new MSG;
    deserialize(data, temp);
    printMsg(temp);

    return 0;
}

void serialize(MSG* msgPacket, char *data)
{
    int *q = (int*)data;    
    *q = msgPacket->type;       q++;    
    *q = msgPacket->priority;   q++;    
    *q = msgPacket->sender;     q++;

    char *p = (char*)q;
    int i = 0;
    while (i < BUFSIZE)
    {
        *p = msgPacket->message[i];
        p++;
        i++;
    }
}

void deserialize(char *data, MSG* msgPacket)
{
    int *q = (int*)data;    
    msgPacket->type = *q;       q++;    
    msgPacket->priority = *q;   q++;    
    msgPacket->sender = *q;     q++;

    char *p = (char*)q;
    int i = 0;
    while (i < BUFSIZE)
    {
        msgPacket->message[i] = *p;
        p++;
        i++;
    }
}

void printMsg(MSG* msgPacket)
{
    cout << msgPacket->type << endl;
    cout << msgPacket->priority << endl;
    cout << msgPacket->sender << endl;
    cout << msgPacket->message << endl;
}

2
一个简单序列化的绝佳示例。 - PhillyNJ
2
你的代码有两个内存泄漏。实际上根本不需要使用new。以下是进一步可能的改进:1. 将MSG作为反序列化函数的返回类型,而不是输出参数;2. 使用struct MSG{...};代替typedef struct MSG{...}MSG;,在C++中是相同的;3. 使用std::stringstd::vector<byte>来保存序列化数据(并且完全放弃那个#define PACKETSIZE),并将其用作serialize函数的返回值,而不是输出参数(或者如果您坚持更改参数为char(&data)[PACKETSIZE])。 - Marian Spanik
2
这假设发送方和接收方都知道结构体的成员。如果每个结构体成员的详细信息也被嵌入并发送,接收方可以使用它来构建适当的指针。一个端口的简单更改而另一个端口不知道会导致很多错误。 - AlphaGoku
3
这是一个好的简单实现,没错。但是,有些情况下这个实现肯定不会起作用:1)如果发送方和接收方没有相同的结构体实现(如@AlphaGoku所指出的那样),2)如果编译器或编译器标志不同(例如,一个编译器决定在结构体元素之间删除填充字节,或者如果一个编译器中int为4个字节而另一个编译器中int为2个字节),或者3)更加隐蔽的是,如果发送方和接收方没有相同的字节序(例如,发送方是小端字节序而接收方是大端字节序)。 - adentinger
1
@AndrewBloom 我同意你的观点,这些问题可以被解决(否则不同的机器就无法通过网络、总线或其他方式进行通信!)。我想表达的是,这个答案只提供了代码,而没有解释其背后的逻辑。目前的代码存在我所解释的问题。 - adentinger
显示剩余4条评论

6

您可以直接执行

struct MyStruct {

    int data;
    char* someNullTerminatedName; // Assuming not larger than 1023 chars

    std::ostream& serialize(std::ostream& os) const {
        char null = '\0';
        os.write((char*)&data, sizeof(data));
        os.write(someNullTerminatedName, strlen(someNullTerminatedName));
        os.write(&null, 1);
        return os;
    }
    std::istream& deserialize(std::istream& is) {
        char buffer[1024];
        int i = 0;
        is.read((char*)&data, sizeof(data));
        do { buffer[i] = is.get(); ++i; } while(buffer[i] != '\0');
        if (someNullTerminatedName != NULL) free(someNullTerminatedName);
        someNullTerminatedName = (char*)malloc(i);
        for (i = 0; buffer[i] != '\0'; ++i) {
            someNullTerminatedName[i] = buffer[i];
        }
        return is;
    }
};

由你来处理字节序和int类型大小的差异等问题。

举个例子:

MyStruct foo, bar;
std::stringstream stream;
foo.serialize(stream);
// ... Now stream.str().c_str() contains a char* buffer representation of foo.
// For example it might contain [ 1f 3a 4d 10 h e l l o w o r l d \0 ]
bar.deserialize(stream);
// ... Now bar is a copy, via a serial stream of data, of foo.

如果您的套接字库通过C++ iostreams公开其接口,那么您甚至不需要stringstream。

6
你还可以查看谷歌的Protocol Buffers,这是一个平台/语言无关的库,用于在主机之间发送数据。
然而,现在的范式已经转变为先编写协议,然后将你的数据结构适配到其中。尽管如此,这样做的优点是它强制使你的软件架构与简单的数据类型相匹配。

3
如果您的结构体是POD类型,您可以使用memcpy函数进行复制: ::memcpy(data, &your_struct, sizeof(YourStruct))); 在接收时反之亦然: ::memcpy(&your_struct, data, sizeof(YourStruct))); 其中,data为char*类型。请注意,您需要分配足够大的内存来存储数据,并在最后删除它。

1
在发送端:struct example{ int a; float b; double c;}; example ex; ex.a = 1; ex.b = 1.02; ex.c = 1.040; ::memcpy(Buffer, &ex, sizeof(example)); retval = send(conn_socket, Buffer, sizeof(Buffer), 0); 在接收端: example ex; retval = recvfrom(msgsock,Buffer, sizeof(Buffer), 0, (struct sockaddr *)&from, &fromlen); ::memcpy(&ex, Buffer, sizeof(example)); printf("%f", ex.c); 输出结果为:-62774385622041925000000000000000000000000000000。这是为什么呢? - Vigo
9
直接拷贝结构体时需非常小心,因为不同的体系结构和编译器有可能会对齐和字节顺序进行不同的处理。 - Nick
我在两个系统上都使用Visual Studio 2005和XP。但为什么会有这种差异呢? - Vigo
你的缓冲区是从哪里来的?你确定它比你的结构体大吗? - 0x26res
如果你在缓冲区中使用malloc分配了60个字节,并且你的缓冲区是char*类型,那么你知道吗?我担心sizeof(buffer)会根据你的平台而变成4个字节/8个字节... - 0x26res
显示剩余2条评论

1

好的,我将从boost网站上获取示例,因为我不明白你无法理解它的内容。
我添加了一些注释和更改,以说明如何通过网络进行传输。网络代码本身不在这里。关于此,你可以查看boost::asio

int main() {
    // create and open a character archive for output
    // we simply use std::strinstream here
    std::stringstream ofs;

    // create class instance
    const gps_position g(35, 59, 24.567f);

    // save data to archive
    {
        boost::archive::text_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
        // archive and stream closed when destructors are called
    }

    // now we have const char* ofs.str().c_str()
    // transfer those bytes via network
    // read them on the other machine

    gps_position newg;
    {
        // create and open an archive for input
        std::stringstream ifs(the_string_we_read_from_the_network);
        boost::archive::text_iarchive ia(ifs);
        // read class state from archive
        ia >> newg;
        // archive and stream closed when destructors are called
    }
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接