鉴于绝大部分文件、网络通信协议、非网络通信协议都有类似的结构{类型,长度,校验,不定长数据,结束标志},再高级点的会包含多个单层TLV,甚至嵌套TLV,状态机流转标志等等。所以编程语言上也需要采用一定的手法。
建立结构
结构体和联合体
例如
//结构体对齐宏
#if defined(__GNUC__)
#define PACKED_STRUCT struct __attribute__((packed))
#endif
#include <stdint.h> //for uint8_t etc.
PACKED_STRUCT Frame {
uint8_t HEADER;
uint8_t ftype;
uint16_t len;
PACKED_STRUCT Payload payload;
};
struct Data {
union {
PACKED_STRUCT Frame frame;
uint8_t rawdata[];
};
} data;
使用技巧
这样就可以通过结构体成员名直接操作
- 写入:
data.frame.ftype = 0x89
直接填充。 - 读取:通过
data.rawdata[i]
直接取到完整的二进制数据。(i表示第i个字节),可以通过%02X 将该rawdata打印出来
调试/单元测试:可以搞个读写回环,保证数据正确后再接入实际功能代码。
位域
//记得使用packed属性告诉编译器不要瞎优化我的结构体
struct __attribute__((packed)) DataBitm {
uint8_t flag: 1;
uint8_t type: 4;
uint8_t len: 3;
uint32_t data[];
}
冒号 :1代表该变量只占用1bit。 例如上面代码,type占用了4bit。我们序列化/反序列化的时候就可以直接取到内存映射上的4bit,而无需做位运算。
序列化(serialization)在计算机科学的资料处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。 依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。反序列化就是它的逆过程。
统一类型
记住这个C99标准库头文件 #include <stdint.h>
,里面的类型 uint8_t到uint32_t足够使用,在所有支持C99编译器上均可使用,包括常见的单片机编译平台,不需要自己定义一些奇奇怪怪又不完全通用的什么u8 u32 U32 UINT32这样的类型。
需要注意大小端问题
这个过程需要注意CPU大小端问题,以下是一些转换宏可以用
//大小端转换,其实就是字节换位
#define BIG_TO_LITTLE_ENDIAN_16(x) ((uint16_t)((((x) & 0xFF00) >> 8) | (((x) & 0x00FF) << 8)))
#define BIG_TO_LITTLE_ENDIAN_32(x) ((uint32_t)((((x) & 0xFF000000) >> 24) | (((x) & 0x00FF0000) >> 8) | (((x) & 0x0000FF00) << 8) | (((x) & 0x000000FF) << 24)))
C99特性 - 柔性数组
另外还可以使用柔性数组来做不定长的协议报文,关键词: C99 柔性数组
,但柔性数组只能用在结构体的尾部,所以如果有帧尾,一般不把帧尾放入结构体里,而是在函数里手动填充。
然后在填充报文的函数里再做switch分支即可完成协议的子类解析。
复制报文数据到发送区的时候一般使用 memcpy(dst,src,size);
状态机流转
最好画一个状态机的图便于分析和修改,这个有很多画流程图的软件都可以做到。
参考案例
接下来我们观赏一段英特尔处理媒体流协议的代码,是RTP协议下的子类。
GITHUB项目地址:https://github.com/OpenVisualCloud/Media-Transport-Library
数据填充
告诉编译器不优化结构体
attribute ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。当然其他编译器也有类似的属性名