有没有关于如何在C中读取和解析二进制数据的库或指南?
我在看一些功能将在网络套接字接收TCP数据包,然后根据规范解析二进制数据,由转码中的信息成为一个更可用的形式.
是否有任何图书馆可以做到这一点,甚至是执行此类事情的入门书?
我不得不同意这里的许多回应.我强烈建议你避免将结构转换为传入数据的诱惑.它似乎很有吸引力,甚至可能适用于您当前的目标,但如果代码被移植到另一个目标/环境/编译器,您将遇到麻烦.原因如下:
Endianness:你现在使用的架构可能是big-endian,但你的下一个目标可能是little-endian.或相反亦然.您可以使用宏(例如ntoh和hton)来克服这个问题,但这是额外的工作,并确保每次引用该字段时都调用这些宏.
对齐:您正在使用的架构可能能够在奇数寻址偏移处加载多字节字,但许多架构不能.如果一个4字节的字跨越一个4字节的对齐边界,那么负载可能会产生垃圾.即使协议本身没有未对齐的字,有时字节流本身也是未对齐的.(例如,虽然IP标头定义将所有4字节字放在4字节边界上,但以太网标头通常会将IP标头本身推送到2字节边界.)
填充:您的编译器可能会选择紧密打包您的结构而不填充,或者它可能会插入填充以处理目标的对齐约束.我在同一个编译器的两个版本之间看到了这种变化.您可以使用#pragmas强制解决问题,但#pragmas当然是特定于编译器的.
位排序:C位域内的位排序是特定于编译器的.另外,对于运行时代码来说,这些位很难"得到".每次在结构中引用位域时,编译器都必须使用一组掩码/移位操作.当然,你将不得不在某些时候进行掩蔽/移动,但如果速度是一个问题,最好不要在每次参考时都这样做.(如果空间是最重要的问题,那么请使用位域,但要小心.)
这一切并不是说"不要使用结构".我最喜欢的方法是声明所有相关协议数据的友好的native-endian结构,没有任何位域并且不关心问题,然后编写一组使用struct作为中间人的对称打包/解析例程.
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! :) }
现在,您的其余代码只是在友好,快速的struct对象中操作数据,并且只在必须与字节流接口时才调用pack/parse.不需要ntoh或hton,也没有位域来减慢代码速度.
在C/C++中执行此操作的标准方法实际上是以'gwaredd'建议的方式转换为结构体
它并不像人们想象的那样不安全.您首先转换为您期望的结构,如在他/她的示例中,然后您测试该结构的有效性.您必须测试最大/最小值,终止序列等.
你在什么平台上必须阅读Unix网络编程,第1卷:套接字网络API.买它,借它,偷它(受害者会理解,这就像偷食物或东西......),但要读它.
在阅读史蒂文斯之后,大部分内容都会更有意义.
让我重申你的问题,看看我是否理解得当.您正在寻找将对数据包进行正式描述的软件,然后生成一个"解码器"来解析这些数据包?
如果是这样,该字段中的引用是PADS.介绍它的一篇好文章是PADS:用于处理Ad Hoc数据的领域专用语言.PADS非常完整,但遗憾的是非自由许可.
有可能的替代方案(我没有提到非C解决方案).显然,没有一个可以被视为完全生产就绪:
binpac
PacketTypes
DataScript
如果你读法语,我在Générationdedécodeursdeformats binaires中总结了这些问题.
根据我的经验,最好的方法是首先编写一组基元,从二进制缓冲区读取/写入某种类型的单个值.这为您提供了高可见性,以及处理任何字节序问题的非常简单的方法:只需使函数正确执行即可.
然后,您可以为struct
每个协议消息定义s,并为每个消息写入pack/unpack(有些人称之为序列化/反序列化)函数.
作为基本情况,提取单个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; }
这里我假设传入数据是big-endian,这在网络协议中很常见(主要是出于历史原因).你当然可以聪明地做一些指针算法并且不需要临时,但我发现这种方式使它更清晰,更容易理解.在调试时,在这种原语中具有最大透明度是一件好事.
下一步是开始定义特定于协议的消息,并编写读/写原语以进行匹配.在这个级别,考虑代码生成; 如果您的协议以一般的机器可读格式描述,您可以从中生成读/写功能,这可以节省很多麻烦.如果协议格式足够聪明,这会更难,但通常是可行的并且强烈建议.
您可能对Google Protocol Buffers感兴趣,它基本上是一个序列化框架.它主要用于C++/Java/Python(这些是Google支持的语言),但一直在努力将其移植到其他语言,包括C语言.(我根本没有使用过C端口,但我负责其中一个C#端口.)