如何在C语言中序列化一个结构体?

11

我有一个结构体对象,包含了几个原始数据类型、指针和结构体指针。我想通过套接字将其发送到另一端,以便在那边使用。由于我希望预先支付序列化成本,所以如何初始化该结构体对象,以便可以立即发送而无需编组?例如

struct A {
    int i;  
    struct B *p;
};

struct B {
    long l;
    char *s[0];
};

struct A *obj; 

// can do I initialize obj?
int len = sizeof(struct A) + sizeof(struct B) + sizeof(?);
obj = (struct A *) malloc(len);
...

write(socket, obj, len);

// on the receiver end, I want to do this
char buf[len];

read(socket, buf, len);
struct A *obj = (struct A *)buf;
int i = obj->i;
char *s = obj->p->s[0];
int i obj.i=1; obj.p.

谢谢。


1
一个注意点:为什么struct A有一个指向struct B的指针,但是分配时好像struct B就放在了struct A的后面?如果你真的想这样分配,那么你至少应该设置obj->p = (void *)((char *)obj + sizeof(struct A))。这还不算你的解决方案本身存在的一般性错误。 - Shahbaz
7
当在结构体内携带指针时,你可能会遇到问题需要解决。由于指针只是一个内存地址,在另一台机器上这个地址没有意义。这意味着你需要序列化指针指向的任何信息(如果结构体内有多级指向对象的指针,这可能会变得麻烦)。你可以考虑让结构体履行契约以确保在序列化之前对指针进行取消引用。 - Porkbutts
5
强烈建议不要尝试通过套接字打包结构。相反,你应该创建一个协议缓冲区,将数据以定义的、具体的和独立于平台的格式存储起来,发送它,并让另一端解包并重构包。从长远来看,这将会少很多麻烦,特别是如果你考虑到了可能会有可变内容的扩展而进行了预先设计。 - WhozCraig
1
两件事。为什么不直接将它发送为字节?除非双方有不同的 CPU,否则这不应该是一个问题。关于指针的另一件事是,即使指针可以在接收端被消耗并在那里使用,发送指针也会引发攻击的风险。 - Alexey Frunze
@Shahbaz 不需要完全相同的一切。编译器和选项可能不同。就 CPU 而言,您只关心:类型大小、字节序以及 CPU 如何处理未对齐的内存访问。在许多情况下,类型大小不是问题,所有 8、16、32 和 64 位类型都可用。 - Alexey Frunze
显示剩余3条评论
6个回答

5
最简单的方法可能是分配一块内存来存储所有内容。例如,考虑以下结构体:
typedef struct A {
  int v;
  char* str;
} our_struct_t;

现在,最简单的方法是创建一个定义好的格式并将其打包成字节数组。我会尝试举个例子:

int sLen = 0;
int tLen = 0;
char* serialized = 0;
char* metadata = 0;
char* xval = 0;
char* xstr = 0;
our_struct_t x;
x.v   = 10;
x.str = "Our String";
sLen  = strlen(x.str); // Assuming null-terminated (which ours is)
tLen  = sizeof(int) + sLen; // Our struct has an int and a string - we want the whole string not a mem addr
serialized = malloc(sizeof(char) * (tLen + sizeof(int)); // We have an additional sizeof(int) for metadata - this will hold our string length
metadata = serialized;
xval = serialized + sizeof(int);
xstr = xval + sizeof(int);
*((int*)metadata) = sLen; // Pack our metadata
*((int*)xval) = x.v; // Our "v" value (1 int)
strncpy(xstr, x.str, sLen); // A full copy of our string

所以这个例子将数据复制到一个大小为2 * sizeof(int) + sLen的数组中,这允许我们有一个整数元数据(即字符串长度)和从结构体中提取的值。要反序列化,你可以想象以下过程:

char* serialized = // Assume we have this
char* metadata = serialized;
char* yval = metadata + sizeof(int);
char* ystr = yval + sizeof(int);
our_struct_t y;
int sLen = *((int*)metadata);
y.v = *((int*)yval);
y.str = malloc((sLen + 1) * sizeof(char)); // +1 to null-terminate
strncpy(y.str, ystr, sLen);
y.str[sLen] = '\0';

你可以看到,我们的字节数组是定义良好的。下面我详细介绍一下结构:

  • 0-3 字节:元数据(字符串长度)
  • 4-7 字节:X.v(数值)
  • 8 - sLen 字节:X.str(数值)

这种定义明确的结构使得你可以在任何环境中重新创建该结构,只要你遵循了定义好的约定。现在,发送这个结构通过套接字取决于你如何开发协议。你可以先发送一个整数数据包,其中包含刚刚构建的数据包的总长度,或者你可以期望元数据先被发送/分离(逻辑上分离,技术上仍然可以同时发送),然后你就知道在客户端需要接收多少数据。例如,如果我接收到元数据值为10,那么我就可以期望后续sizeof(int)+10字节来完成该结构。通常来说,这可能是14字节。

编辑

我将列出一些评论中要求的澄清说明。

我对字符串进行了完整的复制,因此它在(逻辑上)连续的内存中。也就是说,我序列化数据包中的所有数据实际上都是完整的数据 - 没有指针。这样,我们可以通过套接字发送一个单一缓冲区(我们将其称为serialized)。如果仅仅发送指针,接收到指针的用户会期望该指针是一个有效的内存地址。但是,你的内存地址很可能不会完全相同。然而,即使它们相同,他也不会在该地址具有与你完全相同的数据(除非在非常有限和专业的情况下)。

希望通过查看反序列化过程(这是在接收方的端口)来更清楚地说明这一点。注意,我分配了一个结构体来保存发送方发送的信息。如果发送方没有向我发送完整的字符串,而仅仅是发送了内存地址,那么我就无法重建被发送的数据(即使在同一台机器上,我们也拥有两个不同的虚拟内存空间,它们并不相同)。因此,在本质上,指针只是一个适用于原始数据发送者的好映射。

最后,关于“嵌套结构体”,您需要为每个结构体编写多个函数。也就是说,您可以重复使用这些函数。例如,如果我有两个结构体AB,其中A包含B,那么我可以有两个序列化方法:

char* serializeB()
{
  // ... Do serialization
}

char* serializeA()
{
  char* B = serializeB();
  // ... Either add on to serialized version of B or do some other modifications to combine the structures
}

因此,您应该能够使用每个结构体的单个序列化方法。

谢谢,这很棒。如果您能提供更多注释的代码,那就太好了,因为我不理解某些步骤。为什么要完全复制字符串?如果我有一个结构体内嵌在另一个结构体中,我需要为每个不同的结构体编写多个序列化函数,对吗?谢谢。最好的祝福。 - mmm
如果在结构体中有多个char,那么需要两个元数据来引用这两个char的大小吗?谢谢。 - mmm
1
@moursika:元数据被定义为允许可变长度的字符串。如果您创建一个固定长度的数据包,您很可能可以完全消除元数据。但是,如果您想要任意数量和任意长度的字符串,则需要某种方案来实现(可能在元数据或类似的地方),否则您将无法反序列化您的数据。 - RageD

4

除了您的malloc问题之外,这个答案还提到了其他问题。

不幸的是,您无法找到一个好的“技巧”,它仍然与标准兼容。正确地序列化结构的唯一方法是将每个元素分解为字节,将它们写入无符号字符数组中,通过网络发送它们并在另一端把它们拼接起来。简而言之,您需要进行大量的移位和位运算。

在某些情况下,您需要定义一种协议。例如,在您的情况下,您需要确保始终在struct A之后正确放置指针p指向的对象,以便在恢复后可以正确设置指针。每个人都说过不能通过网络发送指针吗?

您可能想要做的另一件类似于协议的事情是在struct B中写入为灵活数组成员s分配的大小。无论您选择哪种序列化数据的布局,显然两侧都应该互相遵循。

重要的是要注意,您不能依赖任何机器特定的东西,例如字节顺序、结构填充或基本类型的大小。这意味着您应该单独序列化元素的每个字段并为它们分配固定数量的字节。


1
你应该以平台无关的方式序列化数据。
这里是一个使用Binn库(我的创作)的示例:
  binn *obj;

  // create a new object
  obj = binn_object();

  // add values to it
  binn_object_set_int32(obj, "id", 123);
  binn_object_set_str(obj, "name", "Samsung Galaxy Charger");
  binn_object_set_double(obj, "price", 12.50);
  binn_object_set_blob(obj, "picture", picptr, piclen);

  // send over the network
  send(sock, binn_ptr(obj), binn_size(obj));

  // release the buffer
  binn_free(obj);

如果您不想使用字符串作为键,可以使用 binn_map,它使用整数作为键。还支持列表。并且您可以在一个结构中插入另一个结构(嵌套结构)。例如:
  binn *list;

  // create a new list
  list = binn_list();

  // add values to it
  binn_list_add_int32(list, 123);
  binn_list_add_double(list, 2.50);

  // add the list to the object
  binn_object_set_list(obj, "items", list);

  // or add the object to the list
  binn_list_add_object(list, obj);

0

解释您的数据并理解您要序列化的内容。您想将一个整数和类型为B的结构体序列化(递归地,您想将一个int、一个long和一个字符串数组序列化)。然后对它们进行序列化。所需长度为sizeof(int)+sizeof(long)+∑strlen(s[i])+1。

另一方面,序列化是一个已经解决的问题(实际上解决了多次)。您确定需要手写一个序列化程序吗?为什么不使用D-Bus或简单的RPC调用?请考虑使用它们。


0

我尝试了@RageD提供的方法,但它没有起作用。

我从反序列化中得到的int值不是原始值。

对于我来说,memcpy()适用于非字符串变量。(您仍然可以使用strcpy()来处理char *)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct A {
    int a;
    char *str;
} test_struct_t;

char *serialize(test_struct_t t) {
    int str_len = strlen(t.str);

    int size = 2 * sizeof(int) + str_len;
    char *buf = malloc(sizeof(char) * (size+1));

    memcpy(buf, &t.a, sizeof(int));
    memcpy(buf + sizeof(int), &str_len, sizeof(int));
    memcpy(buf + sizeof(int) * 2, t.str, str_len);
    buf[size] = '\0';

    return buf;
}

test_struct_t deserialize(char *buf) {
    test_struct_t t;

    memcpy(&t.a, buf, sizeof(int));

    int str_len;
    memcpy(&str_len, buf+sizeof(int), sizeof(int));

    t.str = malloc(sizeof(char) * (str_len+1));
    memcpy(t.str, buf+2*sizeof(int), str_len);
    t.str[str_len] = '\0';

    return t;
}

int main() {
    char str[15] = "Hello, world!";

    test_struct_t t;
    t.a = 123;
    t.str = malloc(strlen(str) + 1);
    strcpy(t.str, str);
    printf("original values: %d %s\n", t.a, t.str);

    char *buf = serialize(t);
    test_struct_t new_t = deserialize(buf);
    printf("new values: %d %s\n", new_t.a, new_t.str);

    return 0;
}

上述代码的输出结果为:

original values: 123 Hello, world!
new values: 123 Hello, world!

-1

@Shahbaz 是对的,我认为你实际上想要这个

int len = sizeof(struct A);
obj = (struct A *) malloc(len);

但是,当您将指针发送到另一台机器时,您也会遇到问题,因为指针所指向的地址在另一台机器上毫无意义。


1
除了强制类型转换是有害的...不要这样做。 - user529758
那样行不通。因为结构体中有指针,序列化版本将会有一个指向(可能)错误的内存地址的指针。 - RageD
@H2CO3 为什么强制类型转换有害?这是为了我的利益。 - spartacus
3
这里解释得很清楚了。 - user529758
@spartacus那为什么不直接使用obj = (struct A*) malloc(sizeof(struct A))呢? - Sam

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