【问题标题】:Best practice for parsing data of mixed type?解析混合类型数据的最佳实践?
【发布时间】:2019-04-13 15:08:50
【问题描述】:

我想知道是否有任何已知的最佳实践/方法来解析混合类型的数据包。

例如,假设数据是 10 个字节,它包括:

字节 0-1:制造商 ID (int)
字节 2:类型 (int)
字节 3-4:设备 ID(ascii char)

我可以简单地将每种数据类型的大小和位置定义为#define,并使用这些定义对其进行解析。但我想知道是否有任何结构可以更好地组织它。

【问题讨论】:

  • 数据是二进制还是文本?
  • 如果您提前知道数据包定义,为什么不直接定义一个包含所有必需字段的自定义结构?
  • 你必须像你写的那样读取数据......因为它看起来像是一个二进制文件......
  • 数据是否通过其他平台的文件传递?发布一些示例输入和导出的输出数据包。否则这太宽泛/不清楚。
  • @Jinsuk,“我可以简单地将每种数据类型的大小和位置定义为#define,并使用这些定义对其进行解析”--> 我真的希望你已经这样做了,发布了该代码然后问关于最佳实践。它将添加信息并制作好帖子。这个太宽泛了。

标签: c parsing


【解决方案1】:

最佳实践是假设来自程序外部的所有数据(例如来自用户、来自文件、来自网络、来自不同进程)可能不正确(并且可能不安全/恶意)。

然后,基于“潜在不正确”的假设,定义类型以区分“未检查的潜在不正确数据”和“已检查的已知正确数据”。对于您的示例,您可以使用 uint8_t packet[10]; 作为未检查数据的数据类型和检查数据的正常结构(带填充和不带 __attribute__((packed));)。这使得程序员在认为自己使用的是安全/经过检查的数据时,很难意外使用不安全的数据。

当然,您还需要代码在这些数据类型之间进行转换,这需要进行尽可能多的完整性检查(并且可能还需要担心字节顺序等问题)。对于您的示例,这些检查可能是:

  • 是任何应该是 ASCII 字符 >= 0x80 的字节,并且它们中的任何一个都是无效的(例如,可能不允许使用退格等控制字符)。
  • 制造商 ID 是否有效(例如,可能存在需要匹配的枚举)
  • 类型是否有效(例如,可能存在需要匹配的枚举)

请注意,此函数应返回某种状态以指示转换是否成功,并且在大多数情况下,此状态还应指示转换不成功时出现的问题(以便调用者可以通知用户或记录问题或以最适合问题的方式处理问题)。例如,“未知的制造商 ID”可能意味着需要更新程序以处理新的制造商并且数据是正确的,而“无效的制造商 ID”意味着数据肯定是错误的。

【讨论】:

  • 哈哈,我看到了__attribute__((packed)),本能地投了反对票——好吧,现在反过来了。
  • 但也许使用uint8_t 输入。
  • @AnttiHaapala:固定uint8_t :-)
【解决方案2】:

像这样:

struct packet {
    uint16_t mfg;
    uint8_t type;
    uint16_t devid;
} __attribute__((packed));

packed 属性(或您平台的等效属性)是避免协议中不存在的隐式填充所必需的。

一旦你有了上面的结构,你只需转换(部分)你从任何地方收到的 char 数组:

char buf[1000];
(struct packet*)(buf + N);

【讨论】:

  • Usual suspects:投诉C编译器可能没有任何打包能力。如果数据包来自另一台机器,字节序可能不同。
  • 是的。像这样:) ...@chux,大概如果你知道数据包的定义,你也知道它使用什么字节序。如果需要,您可以在使用 struct 进行转换之前进行位/字节交换。
  • 另外,packed 是在那些支持打包的编译器中导致未定义行为的可靠方法。将 char 数组转换为数据包违反了严格的别名规则。
  • @AnttiHaapala:我没有投射字符数组。我投了buf + N,这是一个字符*。据我了解,允许强制转换(char* 的严格别名有一个例外)。你怎么看?
  • char * 的严格别名是,但它是相反的方向。它说您可以访问任何对象的字节作为字符类型。它并不是说您可以像任何对象类型一样访问字符数组的元素。
【解决方案3】:

对于完全可移植的版本,我建议您以这种方式阅读:

    struct {
        uint16_t e1;
        uint8_t e2;
        uint16_t e3;
    } d;

    uint8_t *cursor;
    uint8_t rbuf[5];

    read(sock, rbuf, sizeof(rbuf));
    memcpy(&s.e1, &rbuf[0], sizeof(s.e1));
    s.e2 = rbuf[2];
    memcpy(&s.e3, &rbuf[3], sizeof(s.e3));

    s.e1 = ntohs(s.e1);
    s.e3 = ntohs(s.e3);

您可能很想像其他人回答的那样做一些事情,例如:

    struct s {
        uint16_t e1;
        uint8_t e2;
        uint16_t e3;
    } __attribute__((packed));

    struct s d;
    read(sock, &d, sizeof(d));
    s.e1 = ntohs(s.e1);
    s.e3 = ntohs(s.e3);

但是,此代码不是完全可移植的,并且可能会导致问题,因为您正在使用未对齐的内存访问项目 (s.e3),这本身就是未定义的行为。在某些情况下,这种方式可行且可取(缓存污染较少,因为更多结构可以填充不同的缓存行,并且可能代码更简单),但在其他情况下,它可能导致总线错误并使您的代码与某些架构不兼容。

除此之外,您应该遵循其他最佳实践,例如尝试在 read() 调用之间尽可能多地读取结构,制作更好的关于网络到主机字节排序转换的代码......但我认为避免非标准属性应该是第一件事。

请注意,如果您不进行非对齐访问,所有这些(甚至是 __packed__ 属性)都是完全不必要的,您可以阅读以下结构:

struct {
    uint16_t e1;
    uint8_t e2;
    uint8_t e2;
    uint16_t e3;
} d;

read(rsock, &d, sizeof(d));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    • 2021-09-23
    • 2010-09-06
    • 2011-03-06
    相关资源
    最近更新 更多