【问题标题】:how to serialize a struct in c?如何在c中序列化一个结构?
【发布时间】:2013-03-29 17:05:22
【问题描述】:

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

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.

谢谢。

【问题讨论】:

  • 请注意:为什么struct A 有一个指向struct B 的指针,但分配的却好像struct B 放在struct A 之后?如果你真的想这样分配它们,你至少应该设置obj->p = (void *)((char *)obj + sizeof(struct A))。除了您的解决方案中的一般错误之外。
  • 在结构中携带指针时,您将遇到需要解决的问题。由于指针只是一个内存地址,因此该内存地址在另一台机器上没有任何意义。这意味着您将需要序列化指针指向的任何信息(如果您在结构中具有指向对象的多个级别的指针,这可能会变得很麻烦)。您可能需要考虑让您的结构履行合同,以确保在序列化之前取消引用指针。
  • 强烈建议不要尝试跨套接字打包结构。相反,您应该滚动一个协议缓冲区,该缓冲区以定义的、具体的和平台独立格式保存数据,发送它,并让另一端解包并重建包。从长远来看,这将大大减少麻烦,特别是如果您通过考虑潜在的可变内容来设计具有远见的扩展包。
  • 两件事。为什么不将其作为字节发送?除非双方都有不同的CPU,否则应该不是问题。关于指针的另一件事是,即使指针可以在接收端被消耗并以某种方式在那里使用,发送指针也是对攻击的邀请。
  • @AlexeyFrunze,除非双方都有不同的 CPU... 即使这本身就是问题的根源,因为那里有太多的 CPU 类型和变体,即使在相同的 CPU 上只是将它们作为字节发送并接收它们也是不正确的。想想一端有优化,另一端没有。然后很容易就会为结构选择不同的填充并破坏布局。

标签: c networking


【解决方案1】:

最简单的方法可能是分配一块内存来保存所有内容。例如,考虑如下结构:

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 字节。

编辑

我将根据 cmets 的要求列出一些说明。

我对字符串做了一个完整的拷贝,所以它在(逻辑上)连续的内存中。也就是说,我的序列化数据包中的所有数据实际上都是完整数据——没有指针。这样,我们可以通过套接字发送一个缓冲区(我们称之为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
}

所以你应该能够为每个结构使用一个序列化方法。

【讨论】:

  • 谢谢,太好了。如果可以的话,更多注释的代码会很棒,因为我不理解某些步骤。为什么要完整复制字符串?如果我在另一个结构中有一个结构,我必须为每个不同的结构创建多个序列化函数,对吗?致以最诚挚的问候
  • 如果结构内有多个 char* ,则需要 2 个元数据来引用两个 char* 的大小?谢谢
  • @moursika:元数据被定义为允许可变长度的字符串。如果您创建一个固定长度的数据包,您很可能会完全消除元数据。但是,是的,如果您想要任意数量的任意长度字符串,您将需要一些方案来执行此操作(可能在元数据或类似的东西中),否则您将无法反序列化您的数据。
【解决方案2】:

这个答案不包括你的malloc 的问题。

不幸的是,您找不到仍然与标准兼容的好技巧唯一正确序列化结构的方法是将每个元素分别分解为字节,将它们写入 unsigned char 数组,通过网络发送它们并在另一端将这些片段重新组合在一起。简而言之,您将需要大量的移位和按位操作。

在某些情况下,您需要定义一种协议。例如,在您的情况下,您需要确保始终将对象p 指向struct A 之后,因此一旦恢复,您就可以正确设置指针。大家是不是已经说够了不能通过网络发送指针?

您可能想做的另一件事是在struct B 中写入为灵活数组成员s 分配的大小。无论您为序列化数据选择何种布局,显然双方都应该尊重。

请务必注意,您不能依赖任何特定于机器的内容,例如字节顺序、结构填充或基本类型的大小。这意味着您应该分别序列化元素的每个字段并为其分配固定数量的字节。

【讨论】:

    【解决方案3】:

    您应该以独立于平台的方式序列化数据。

    这是一个使用 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);
    

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        我尝试了@RageD提供的方法,但没有成功。

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

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

        #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!
        

        【讨论】:

          【解决方案6】:

          @Shahbaz 是对的,我认为你真的想要这个

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

          但是在向另一台机器发送指针时也会遇到问题,因为指针指向的地址在另一台机器上没有任何意义。

          【讨论】:

          • 除非演员表是有害的......不要这样做。
          • 那行不通。由于结构中有指针,序列化版本将有一个指向(可能)错误内存地址的指针。
          • @H2CO3 为什么这种铸件有害?为了我自己的利益。
          • @spartacus 那为什么不直接obj = (struct A*) malloc(sizeof(struct A)) 呢?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-09-21
          • 1970-01-01
          • 2010-09-27
          • 1970-01-01
          • 1970-01-01
          • 2017-08-09
          相关资源
          最近更新 更多