20

是否有任何关于如何在 C 中读取和解析二进制数据的库或指南?

我正在研究一些功能,这些功能将在网络套接字上接收 TCP 数据包,然后根据规范解析二进制数据,通过代码将信息转换为更可用的形式。

是否有任何图书馆可以做到这一点,或者甚至是执行这类事情的入门书?

4

9 回答 9

34

我不得不不同意这里的许多回应。我强烈建议您避免将结构强制转换为传入数据的诱惑。它看起来很有说服力,甚至可能适用于您当前的目标,但是如果将代码移植到另一个目标/环境/编译器,您就会遇到麻烦。几个原因:

Endianness:您现在使用的架构可能是大端,但您的下一个目标可能是小端。或相反亦然。您可以使用宏(例如,ntoh 和 hton)来克服这个问题,但这是额外的工作,并且您必须确保每次引用该字段时都调用这些宏。

对齐:您使用的体系结构可能能够在奇地址偏移处加载多字节字,但许多体系结构不能。如果 4 字节字跨越 4 字节对齐边界,则加载可能会拉垃圾。即使协议本身没有错位字,有时字节流本身也会错位。(例如,虽然 IP 报头定义将所有 4 字节字放在 4 字节边界上,但以太网报头通常会将 IP 报头本身推到 2 字节边界上。)

Padding:您的编译器可能会选择在没有填充的情况下紧密打包您的结构,或者它可能会插入填充以处理目标的对齐约束。我已经看到同一编译器的两个版本之间的这种变化。您可以使用#pragmas 来强制解决问题,但#pragmas 当然是特定于编译器的。

位排序:C 位域内的位排序是特定于编译器的。另外,对于您的运行时代码,这些位很难“获取”。每次引用结构内的位域时,编译器都必须使用一组掩码/移位操作。当然,您将不得不在某些时候进行掩蔽/移动,但如果速度是一个问题,最好不要在每个参考点都这样做。(如果空间是最重要的问题,那么使用位域,但要小心。)

这并不是说“不要使用结构”。我最喜欢的方法是声明所有相关协议数据的友好本机字节序结构,没有任何位域并且不关心问题,然后编写一组使用该结构作为中间人的对称打包/解析例程。

typedef struct _MyProtocolData
{
    Bool myBitA;  // Using a "Bool" type wastes a lot of space, but it's fast.
    Bool myBitB;
    Word32 myWord;  // You have a list of base types like Word32, right?
} MyProtocolData;

Void myProtocolParse(const Byte *pProtocol, MyProtocolData *pData)
{
    // Somewhere, your code has to pick out the bits.  Best to just do it one place.
    pData->myBitA = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_A_MASK >> MY_BIT_A_SHIFT;
    pData->myBitB = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_B_MASK >> MY_BIT_B_SHIFT;

    // Endianness and Alignment issues go away when you fetch byte-at-a-time.
    // Here, I'm assuming the protocol is big-endian.
    // You could also write a library of "word fetchers" for different sizes and endiannesses.
    pData->myWord  = *(pProtocol + MY_WORD_OFFSET + 0) << 24;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 1) << 16;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 2) << 8;
    pData->myWord += *(pProtocol + MY_WORD_OFFSET + 3);

    // You could return something useful, like the end of the protocol or an error code.
}

Void myProtocolPack(const MyProtocolData *pData, Byte *pProtocol)
{
    // Exercise for the reader!  :)
}

现在,您的其余代码仅在友好、快速的结构对象中操作数据,并且仅在您必须与字节流交互时才调用 pack/parse。不需要 ntoh 或 hton,也不需要位域来减慢您的代码速度。

于 2008-11-27T16:02:07.287 回答
14

在 C/C++ 中执行此操作的标准方法实际上是按照“gwaredd”的建议转换为结构

它并不像人们想象的那样不安全。您首先转换为您期望的结构,如他/她的示例中所示,然后测试该结构的有效性。您必须测试最大值/最小值、终止序列等。

无论您使用什么平台,都必须阅读Unix Network Programming, Volume 1: The Sockets Networking API。买,借,偷(受害者会明白,这就像偷食物什么的……),但一定要读。

在阅读史蒂文斯之后,大部分内容会更有意义。

于 2008-11-26T17:31:59.260 回答
12

让我重申你的问题,看看我是否理解正确。您正在寻找可以对数据包进行正式描述然后生成“解码器”来解析此类数据包的软件?

如果是这样,该字段中的引用是PADS。介绍它的一篇好文章是PADS: A Domain-Specific Language for Processing Ad Hoc Data。PADS 非常完整,但不幸的是在非免费许可证下。

有可能的替代方案(我没有提到非 C 解决方案)。显然,没有一个可以被视为完全生产就绪:

如果您阅读法语,我在Génération de décodeurs de format binaires中总结了这些问题。

于 2008-11-27T07:22:54.057 回答
10

以我的经验,最好的方法是首先编写一组原语,从二进制缓冲区读取/写入某种类型的单个值。这为您提供了高可见性,以及处理任何字节顺序问题的非常简单的方法:只需让函数正确执行即可。

然后,您可以为struct每个协议消息定义 s,并为每个消息编写打包/解包(有些人称它们为序列化/反序列化)函数。

作为基本情况,提取单个 8 位整数的原语可能如下所示(假设char主机上有 8 位,如果需要,您也可以添加一层自定义类型来确保这一点):

const void * read_uint8(const void *buffer, unsigned char *value)
{
  const unsigned char *vptr = buffer;
  *value = *buffer++;
  return buffer;
}

在这里,我选择通过引用返回值,并返回一个更新后的指针。这是一个口味问题,您当然可以返回值并通过引用更新指针。读取函数更新指针以使这些指针可链接是设计的关键部分。

现在,我们可以编写一个类似的函数来读取 16 位无符号数:

const void * read_uint16(const void *buffer, unsigned short *value)
{
  unsigned char lo, hi;

  buffer = read_uint8(buffer, &hi);
  buffer = read_uint8(buffer, &lo);
  *value = (hi << 8) | lo;
  return buffer;
}

这里我假设传入的数据是大端的,这在网络协议中很常见(主要是出于历史原因)。你当然可以变得聪明,做一些指针运算并消除对临时的需要,但我发现这种方式使它更清晰,更容易理解。在调试时,在这种原语中具有最大的透明度可能是一件好事。

下一步将开始定义您的协议特定消息,并编写读/写原语以进行匹配。在那个层面上,考虑代码生成;如果您的协议以某种通用的、机器可读的格式描述,您可以从中生成读/写函数,这样可以省去很多麻烦。如果协议格式足够聪明,这会更难,但通常可行且强烈推荐。

于 2008-11-27T07:44:31.143 回答
5

您可能对Google Protocol Buffers感兴趣,它基本上是一个序列化框架。它主要用于 C++/Java/Python(这些是 Google 支持的语言),但正在努力将其移植到其他语言,包括C。(我根本没有使用过 C 端口,但我负责其中一个 C# 端口。)

于 2008-11-26T17:21:06.677 回答
3

您实际上并不需要在 C 中解析二进制数据,只需将一些指针指向您认为应该是的任何内容。

struct SomeDataFormat
{
    ....
}

SomeDataFormat* pParsedData = (SomeDataFormat*) pBuffer;

请注意字节序问题、类型大小、读取缓冲区末尾等

于 2008-11-26T17:12:36.553 回答
2

解析/格式化二进制结构是在 C 中比在高级/托管语言中更容易完成的极少数事情之一。您只需定义一个与您要处理的格式相对应的结构,该结构就是解析器/格式化程序。这是可行的,因为 C 中的结构表示精确的内存布局(当然,它已经是二进制的)。另请参阅 kervin 和 gwaredd 的回复。

于 2008-11-26T18:04:46.760 回答
1

我不太明白你在找什么样的图书馆?将接受任何二进制输入并将其解析为未知格式的通用库?我不确定是否有这样的库可以以任何语言存在。我认为您需要稍微详细说明您的问题。

编辑
好的,所以在阅读Jon 的回答之后似乎有一个库,它更像是代码生成工具。但正如许多人所说,只是将数据转换为适当的数据结构,适当小心,即使用打包结构并处理字节序问题,你很好。将这样的工具与 C 一起使用只是一种矫枉过正。

于 2008-11-26T17:22:23.957 回答
1

基本上是关于铸造struct工作的建议,但请注意,数字在不同的架构上可能以不同的方式表示。

为了处理字节序问题,引入了网络字节顺序 - 通常的做法是在发送数据之前将数字从主机字节顺序转换为网络字节顺序,并在接收时转换回主机顺序。请参阅函数htonlhtons和。ntohlntohs

并真正考虑 kervin 的建议 - 阅读UNP。你不会后悔的!

于 2008-11-27T07:21:58.880 回答