FlatBuffer內部解析原理簡介

簡介

Flatbuffer 是一個高效的跨平臺、支持多種語言序列化數據的庫。最初由谷歌爲遊戲開發而開發的,現在也用於多種對性能要求嚴苛的應用中。FlatBuffer有以下優點(直接翻譯官網文檔,詳細介紹看這裏):

  1. 可不需要解析、拆包,而直接訪問序列化後的數據;
  2. 內存利用率高以及讀取速度快;
  3. 靈活性;
  4. 生成代碼量小;
  5. 強類型;
  6. 使用方便;
  7. 代碼跨平臺無其他依賴。

關於使用方法就不多介紹了,對於源碼編譯、Schema的編譯官網有詳細介紹,點擊這裏可以瞭解更多使用方法。這裏想介紹的是解析一個已經序列化的數據文件的方法。其實如果只是想要知道怎麼使用FlatBuffer,這部分內容是沒必要了解的,但是好奇心的驅使下,還是想一探究竟。這是一篇個人理解文章,如果想看官方介紹請戳這裏

閒言少敘,書歸正傳。

鋪墊

爲了便於理解,我在Ubuntu16.04下下載源碼並編譯出了FlatBuffer編譯器flatc, 通過--binary選項將一個JSON數據文件編譯成一個二進制文件Monster.bin,用到的Schema和數據如下:

Monster.fbs
// Example IDL file for our monster's schema.
namespace MyGame.Sample;
enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment { Weapon } // Optionally add more tables.
struct Vec3 {
  x:float;
  y:float;
  z:float;
}
table Monster {
  pos:Vec3; // Struct.
  mana:short = 150;
  hp:short = 30;
  name:string;
  friendly:bool = false (deprecated);
  inventory:[ubyte];  // Vector of scalars.
  color:Color = Blue; // Enum.
  weapons:[Weapon];   // Vector of tables.
  equipped:Equipment; // Union.
  path:[Vec3];        // Vector of structs.
}
table Weapon {
  name:string;
  damage:short;
}
root_type Monster;
Monster.json
{ pos: { x: 1, y: 2, z: 3 }, name: "fred", hp: 30}
二進制文件內容

序列化後的Monster.json

正文

概念介紹

FlatBuffer中有幾個主要的概念:

  1. Struct: 在Schema中表現爲struct定義。主要是爲了獲得極致的訪問效率,Struct中的內容緊湊的排列在一起,內聯在其所屬的父類容器中,也就是可以直接得到其中的內容而不需要額外計算保存的偏移地址;舉個不恰當的栗子:有個強迫症患者他的書櫃中有《語文》、《數學》、《英語》三本書,這三本書在強迫症的作用下一定是按照這個順序擺放的,任何時候你找到語文書的位置,你肯定知道下一本是數學,就算閉着眼你也知道拿到的第三本肯定是英語。
  2. Table: 在Schema中表現爲table定義。table中定義的元素存儲的位置是不確定的,對於同一個數據不同的實現方法對於同一字段的存放位置可能完全不一樣,爲了確定某一字段的具體位置,需要通過一個VTable來獲取。這裏再來個栗子:假設有兩個圖書館,擁有數量和內容一樣的書,哪個強迫症也是不可要求兩個圖書館同樣的書擺放在一樣的位置。這回就算你找到了語文書的位置,你也是不能保證下一本就是數學,閉着眼睛拿也可能拿到《母豬的產後護理》,這就亂套了,那怎麼辦,只能通過記在一張紙上的索書號來找,這張紙就相當於vtable吧。table通過用它的起始地址減去開頭用一個soffset_t類型的數據來得到Vtable的地址,例如,table起始地址是16,soffset_t類型的值爲12,那麼table所對應的vtable的起始地址addr=16-12=4,即從地址4開始。
  3. Vector: 在Schema中表現爲一個列表。String就是一個特化的Vector,一個用於保存字符的vector。
  4. Offset: 用於找到各種內容的偏移地址。類似於軍師給了我一個錦囊讓我到某地;到達某地後我又打開第二個錦囊,按照錦囊的內容再幹點啥,然後敵軍的人頭就是我的了。每個文件開頭用一個uoffset_t類型來表示跟對象的的其實位置,例如在Monster.fbs中根對象就是root_type所指得table Monster
  5. Vtable: 一個保存 table各個字段相對於table起始地址的偏移地址。vtable中所有元素類型都是voffset_t, 也就是uint16_t,如下圖。第一個元素用來表示整個vtable的字節數,包括第一個元素所佔的;第二個元素表示整個vtable所表示的對象的大小;接下來的元素便是按照Schema中定義的順序,例如Schema順序定義了a,b,c三個字段,那麼接下來的三個元素就分別指示a,b,c相對於table起始地址的偏移地址,如果某個元素爲0,那麼說明這個元素在table中沒有定義,獲取到的將是默認值。如果某個字段已經超出了vtable的長度,也說明在這個對象中沒有該字段,例如如果vtable中只有a,b兩個元素,那麼說明c定義的字段使用了默認值。
    vtable

栗子來啦

有以上概念,根據鋪墊一節中所給出的數據,我們可以從凡人脫胎成CPU了。

  1. 二進制文件開頭四個字節表示跟對象地址,因爲數據是以小端格式存儲的那麼0x10000000轉換成十進制就是16,說明根對象從第17個字節開始;
  2. table對象開頭四個字節爲0x0c000000,轉換成十進制就是12,16-12=4,所以vtable地址爲4;
  3. 找到地址4,第一個元素爲vtable長度,由於vtable元素的類型都是voffset_t,在這裏就是兩個字節,值爲0x0c00,轉換爲十進制爲12,說明vtable中一共包含12個字節;第三第四個字節這裏用不到;
  4. vtable第3,4個字節爲0x0800,轉成十進制爲8,也就是從table起始位置16開始,再偏移8個字節得到的是Schema中定義的Vec字段,因爲Vec是struct,是內聯的,所以可以知道從24到35這12個字節每四個字節一組依次存儲Vec中的三個浮點數x,y,z,分別等於1.0,2.0,3.0;
  5. vtable第5,6個字節值爲0,說明table Monster的第二個字段mana在這個二進制文件中不存在,獲取到的將是默認值150;
  6. vtable第7,8個字節的值爲6,也就是從table起始位置16開始,再偏移6個字節得到的是Schema中定義的hp字段的內容,short類型佔兩個字節,也就是從地址22開始區兩個字節得到0x1e00,轉成十進制爲30;
  7. vtable第9,10個字節值爲20,從table起始位置16開始,再偏移20個字節,也就是地址36開始得到的是Schema中定義的name字段的內容,由於name是一個vector,所以得到的內容將是其相對於地址36的偏移,0x04000000轉爲十進制是4,即name實際內容的開始地址爲40;
  8. vector開頭固定四個字節表示vector長度,多以40至43四個字節存儲長度,0x04000000轉爲十進制爲4,表示這個vecto裏面有四個元素,由於此vector中保存的是是字節,所以44至47保存的是f r e d這四個字符的ASCII碼點。剩下多餘的字節分別是一個終止符和填充字符。
    至此,我們對這個二進制文件的解析結束。

總結

  1. FlatBuffer以小端格式(little-endian)存儲;
  2. 二進制開頭uoffset_t保存跟對象其實地址,具體佔字節數視情況而定;
  3. 每個對象開頭soffset_t保存其vtable的偏移地址,計算方法是vtable = uoffset_t - soffset_t
  4. 對象中嵌套的對象,其字段存儲的值是該對象的地址而不是實際對象內容,例如:table對象中含有的一個name字段是vector對象,那麼時期vector的存儲對象地址爲table_addr + vtable[name] + name,例如:table其實地址爲16,查vtable表name字段地址偏移爲6,讀取16 + 6 處內容爲16,則實際Vector存儲地址爲16 + 6 + 16 = 38,即地址38爲Vector起始地址;
  5. 對於vector中存儲的table的值得查找,也是從該地址開始,通過緊跟着的uoffset_t獲取該對象的跟位置,進而獲取vtable。例如Vector weapons中保存的是table Weapon,當找到weapons的第一個元素的地址,假設地址爲x,那麼第一個元素對象的根即等於x + uoffset_t,例如通過上面第四點算得Vector weapons實際存儲地址爲96,第一個元素起始地址爲100即爲x,若緊隨地址100的uoffset_t爲24則第一個table Weapon的根地址爲124;
    最後用一張圖結束:
    層級獲取內容示意圖

本文首發於個人公衆號TensorBoy。如果你覺得內容還不錯,歡迎分享並關注我的公衆號TensorBoy,掃描下方二維碼獲取更多精彩原創內容!
公衆號二維碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章