鑑於絕大部分文件、網絡通信協議、非網絡通信協議都有類似的結構{類型,長度,校驗,不定長數據,結束標誌},再高級點的會包含多個單層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特有的語法。當然其他編譯器也有類似的屬性名