FlatBuffers 是一個序列化開源庫,實現了與 Protocol Buffers,Thrift,Apache Avro,SBE 和 Cap'n Proto 相似的序列化格式,主要由 Wouter van Oortmerssen 編寫,並由 Google 開源。Oortmerssen 最初爲 Android 遊戲和注重性能的應用而開發了FlatBuffers。如今它具備C ++,C#,C,Go,Java,PHP,Python 和 JavaScript 的端口。html
FlatBuffer 是一個二進制 buffer,它使用 offset 組織嵌套對象(struct,table,vectors,等),可使數據像任何基於指針的數據結構同樣,就地訪問數據。然而 FlatBuffer 與大多數內存中的數據結構不一樣,它使用嚴格的對齊規則和字節順序來確保 buffer 是跨平臺的。此外,對於 table 對象,FlatBuffers 提供前向/後向兼容性和 optional 字段,以支持大多數格式的演變。android
FlatBuffers 的主要目標是避免反序列化。這是經過定義二進制數據協議來實現的,一種將定義好的將數據轉換爲二進制數據的方法。由該協議建立的二進制結構能夠 wire 發送,而且無需進一步處理便可讀取。相比較而言,在傳輸 JSON 時,咱們須要將數據轉換爲字符串,經過 wire 發送,解析字符串,並將其轉換爲本地對象。Flatbuffers 不須要這些操做。你用二進制裝入數據,發送相同的二進制文件,並直接從二進制文件讀取。git
儘管 FlatBuffers 有本身的接口定義語言來定義要與之序列化的數據,但它也支持 Protocol Buffers 中的 .proto
格式。github
在 schema 中定義對象類型,而後能夠將它們編譯爲 C++ 或 Java 等各類主流語言,以實現零開銷讀寫。FlatBuffers 還支持將 JSON 數據動態地分析到 buffer 中。算法
除了解析效率之外,二進制格式還帶來了另外一個優點,數據的二進制表示一般更具備效率。咱們可使用 4 字節的 UInt 而不是 10 個字符來存儲 10 位數字的整數。編程
JSON 是一種獨立於語言存在的數據格式,可是它解析數據並將之轉換成如 Java 對象時,會消耗咱們的時間和內存資源。客戶端解析一個 20KB 的 JSON 流差很少須要 35ms,而 UI 一次刷新的時間是 16.6ms。在高實時遊戲中,是不能有任何卡頓延遲的,因此須要一種新的數據格式;服務器在解析 JSON 時候,有時候會建立很是多的小對象,對於每秒要處理百萬玩家的 JSON 數據,服務器壓力會變大,若是每次解析 JSON 都會產生不少小對象,那麼海量玩家帶來的海量小對象,在內存回收的時候可能會形成 GC 相關的問題。Google 員工 Wouter van Oortmerssen 爲了解決遊戲中性能的問題,因而開發出了 FlatBuffers。(注:Protocol buffers 是 created by google,而 FlatBuffers 是 created at google)json
幾年前,Facebook 宣稱本身的 Android app 在數據處理的性能方面有了極大的提高。在幾乎整個 app 中,他們放棄了 JSON 而用 FlatBuffers 取而代之。api
FlatBuffers (9490 star) 和 Cap'n Proto (5527 star)、simple-binary-encoding (1351 star) 同樣,它支持「零拷貝」反序列化,在序列化過程當中沒有臨時對象產生,沒有額外的內存分配,訪問序列化數據也不須要先將其複製到內存的單獨部分,這使得以這些格式訪問數據比須要格式的數據(如JSON,CSV 和 protobuf)快得多。數組
FlatBuffers 與 Protocol Buffers 確實比較類似,主要的區別在於 FlatBuffers 在訪問數據以前不須要解析/解包。二者代碼也是一個數量級的。可是 Protocol Buffers 既沒有可選的文本導入/導出功能,也沒有 union 這個語言特性,這兩點 FlatBuffers 都有。安全
FlatBuffers 專一於移動硬件(內存大小和內存帶寬比桌面端硬件更受限制),以及具備最高性能需求的應用程序:遊戲。
說了這麼多,讀者會疑問,FlatBuffers 使用的人多麼?Google 官方頁面上提了 3 個著名的 app 和 1 個框架在使用它。
BobbleApp,印度第一貼圖 App。BobbleApp 中使用 FlatBuffers 後 App 的性能明顯加強。
Facebook 使用 FlatBuffers 在 Android App 中進行客戶端服務端的溝通。他們寫了一篇文章《Improving Facebook's performance on Android with FlatBuffers》來描述 FlatBuffers 是如何加速加載內容的。
Google 的 Fun Propulsion Labs 在他們全部的庫和遊戲中大量使用 FlatBuffers。
Cocos2d-X,第一開源移動遊戲引擎,使用 FlatBuffers 來序列化全部的遊戲數據。
因而可知,在遊戲類的 app 中,普遍使用 FlatBuffers。
編寫一個 schema 文件,容許您定義您想要序列化的數據結構。字段能夠有標量類型(全部大小的整數/浮點數),也能夠是字符串,任何類型的數組,引用另外一個對象,或者一組可能的對象(Union)。字段能夠是可選 optional 的也能夠有默認值,因此它們不須要存在於每一個對象實例中。
舉個例子:
// example IDL file
namespace MyGame;
attribute "priority";
enum Color : byte { Red = 1, Green, Blue }
union Any { Monster, Weapon, Pickup }
struct Vec3 {
x:float;
y:float;
z:float;
}
table Monster {
pos:Vec3;
mana:short = 150;
hp:short = 100;
name:string;
friendly:bool = false (deprecated, priority: 1);
inventory:[ubyte];
color:Color = Blue;
test:Any;
}
root_type Monster;
複製代碼
上面是 schema 語言的語法,schema 又名 IDL(Interface Definition Language,接口定義語言),代碼和 C 家族的語言很是像。
在 FlatBuffers 的 schema 文件中,有兩個很是重要的概念,struct 和 table 。
Table 是在 FlatBuffers 中定義對象的主要方式,由一個名稱(這裏是 Monster)和一個字段列表組成。每一個字段都有一個名稱,一個類型和一個可選的默認值(若是省略,它默認爲 0 / NULL)。
Table 中每一個字段都是可選 optional 的:它沒必要出如今 wire 表示中,而且能夠選擇省略每一個單獨對象的字段。所以,您能夠靈活地添加字段而不用擔憂數據膨脹。這種設計也是 FlatBuffer 的前向和後向兼容機制。
假設當前 schema 是以下:
table { a:int; b:int; }
複製代碼
如今想對這個 schema 進行更改。
有幾點須要注意:
只能在表定義的末尾添加新的字段。舊數據仍會正確讀取,並在讀取時爲您提供默認值。舊代碼將簡單地忽略新字段。若是但願靈活地使用 schema 中字段的任何順序,您能夠手動分配 ids(很像 Protocol Buffers),請參閱下面的 id 屬性。
舉例:
table { a:int; b:int; c:int; }
複製代碼
這樣作能夠。舊的 schema 讀取新的數據結構會忽略新字段 c 的存在。新的 schema 讀取舊的數據,將會取到 c 的默認值(在此狀況下爲 0,由於未指定)。
table { c:int a:int; b:int; }
複製代碼
在前面添加新字段是不容許的,由於這會使 schema 新舊版本不兼容。用老的代碼讀取新的數據,讀取新字段 c 的時候,其實讀到的是老的 a 字段。用新代碼讀取老的數據,讀取老字段 a 的時候,其實讀到的是老的 b 字段。
table { c:int (id: 2); a:int (id: 0); b:int (id: 1); }
複製代碼
這樣作是可行的。若是您的意圖是以有意義的方式對語義進行排序/分組,您可使用顯式標識賦值來完成。引入 id 之後,table 中的字段順序就無所謂了,新的與舊的 schema 徹底兼容,只要咱們保留 id 序列便可。
不能從 schema 中刪除再也不使用的字段,但能夠簡單地中止將它們寫入數據中,和寫入和刪除字段,兩種作法幾乎相同的效果。此外,能夠將它們標記爲 deprecated,如上例所示,被標記的字段不會再生成 C ++ 的訪問器,從而強制該字段再也不被使用。 (當心:這可能會破壞代碼!)。
table { b:int; }
複製代碼
這種刪除字段的方法不可行。咱們只能經過棄用來刪除某個字段,而無論是否使用了明確的ID 標識。
table { a:int (deprecated); b:int; }
複製代碼
上面這樣的作法也是能夠的。舊的 schema 讀取新的數據結構會得到 a 的默認值,由於它不存在。新的 schema 代碼不能讀取也不能寫入 a(現有代碼嘗試這樣作會致使編譯錯誤),但仍能夠讀取舊數據(它們將忽略該字段)。
能夠更改字段名稱和 table 名稱,若是您的代碼能夠正常工做,那麼您也能夠更改它們。
table { a:uint; b:uint; }
複製代碼
直接修改字段的類型,這樣作可能可行,也有狀況不行。只有在類型改變是相同大小的狀況下,是可行的。若是舊數據不包含任何負數,這將是安全的,若是包含了負數,這樣改變會出現問題。
table { a:int = 1; b:int = 2; }
複製代碼
這樣修改不可行。任何寫入數值爲 0 的舊數據都不會再寫入 buffer,並依賴於從新建立的默認值。如今這些值將顯示爲1和2。有些狀況下可能不會出錯,但必須當心。
table { aa:int; bb:int; }
複製代碼
上面這種修改方法,修改原來的變量名之後,可能會出現問題。因爲已經重命名了字段,這將破壞全部使用此版本 schema 的代碼(和 JSON 文件),這與實際的二進制緩衝區不兼容。
table 是 FlatBuffers 的基石,由於對於大多數須要序列化應用來講,數據結構改變是必不可少的。一般狀況下,處理數據結構的變動在大多數序列化解決方案的解析過程當中能夠透明地完成的。可是一個 FlatBuffer 在被訪問以前不會被分析。
爲了解決數據結構變動的問題,table 經過 vtable 間接訪問字段。每一個 table 都帶有一個 vtable(能夠在具備相同佈局的多個 table 之間共享),而且包含存儲此特定類型 vtable 實例的字段的信息。vtable 還可能代表該字段不存在(由於此 FlatBuffer 是使用舊版本的軟件編寫的,僅僅由於信息對於此實例不是必需的,或者被視爲已棄用),在這種狀況下會返回默認值。
table 的內存開銷很小(由於 vtables 很小而且共享)訪問成本也很小(間接訪問),可是提供了很大的靈活性。table 甚至可能比等價的 struct 花費更少的內存,由於字段在等於默認值時不須要存儲在 buffer 中。
structs 和 table 很是類似,只是 structs 沒有任何字段是可選的(因此也沒有默認值),字段可能不會被添加或被棄用。結構可能只包含標量或其餘結構。若是肯定之後不會進行任何更改(如 Vec3 示例中很是明顯),請將其用於簡單對象。structs 使用的內存少於 table,而且訪問速度更快(它們老是以串聯方式存儲在其父對象中,而且不使用虛擬表)。
structs 不提供前向/後向兼容性,但佔用內存更小。對於不太可能改變的很是小的對象(例如座標對或RGBA顏色)存成 struct 是很是有用的。
FlatBuffers 支持的 標量 類型有如下幾種:
括號裏面的名字對應的是類型的別名。
FlatBuffers 支持的 非標量 類型有如下幾種:
標量類型的字段有默認值,非標量的字段(string/vector/table)若是沒有值的話,默認值爲 NULL。
一旦一個類型聲明瞭,儘可能不要改變它的類型,一旦改變了,極可能就會出現錯誤。上面也提到過了,若是把 int 改爲 uint,數據若是有負數,那麼就會出錯。
定義一系列命名常量,每一個命名常量能夠分別給一個定值,也能夠默認的從前一個值增長一。默認的第一個值是 0。正如在上面例子中看到的枚舉聲明,使用:(上面例子中是 byte 字節)指定枚舉的基本整型,而後肯定用這個枚舉類型聲明的每一個字段的類型。
一般,只應添加枚舉值,不要去刪除枚舉值(對枚舉不存在棄用一說)。這須要開發者代碼經過處理未知的枚舉值來自行處理向前兼容性的問題。
這個是 Protocol buffers 中還不支持的類型。
union 是 C 語言中的概念,一個 union 中能夠放置多種類型,共同使用一個內存區域。
可是在 FlatBuffers 中,Unions 能夠像 Enums 同樣共享許多屬性,但不是常量的新名稱,而是使用 table 的名稱。能夠聲明一個 Unions 字段,該字段能夠包含對這些類型中的任何一個的引用,即這塊內存區域只能由其中一種類型使用。另外還會生成一個帶有後綴 _type
的隱藏字段,該字段包含相應的枚舉值,從而能夠在運行時知道要將哪些類型轉換爲類型。
union 跟 enum 比較相似,可是 union 包含的是 table,enum 包含的是 scalar或者 struct。
Unions 是一種可以在一個 FlatBuffer 中發送多種消息類型的好方法。請注意,由於union 字段其實是兩個字段(有一個隱藏字段),因此它必須始終是表的一部分,它自己不能做爲 FlatBuffer 的 root。
若是須要以更開放的方式區分不一樣的 FlatBuffers,例如文件,請參閱下面的文件標識功能。
最後還有一個實驗功能,只在 C++ 的版本實現中提供支持,如上面例子中,把 [Any] (聯合體數組) 做爲一個類型添加到了 Monster 的 table 定義中。
這聲明瞭您認爲是序列化數據的根表(或結構)。這對於解析不包含對象類型信息的 JSON 數據尤其重要。
一般狀況下,FlatBuffer 二進制緩衝區不是自描述的,即它須要您瞭解其 schema 才能正確解析數據。可是若是你想使用一個 FlatBuffer 做爲文件格式,那麼可以在那裏有一個「魔術數字」是很方便的,就像大多數文件格式同樣,可以作一個完整的檢查來看看你是否閱讀你指望的文件類型。
FlatBuffer 雖然容許開發者能夠在 FlatBuffer 前加上本身的文件頭,但 FlatBuffers 有一種內置方法,可讓標識符佔用最少空間,而且還能使 FlatBuffer 與不具備此類標識符的 FlatBuffer 相互兼容。
聲明文件格式的方法相似於 root_type:
file_identifier "MYFI";
複製代碼
標識符必須正好 4 個字符。這 4 個字符將做爲 buffer 末尾的 [4,7] 字節。
對於具備這種標識符的任何 schema,flatc 會自動將標識符添加到它生成的任何二進制文件中(帶-b),而且生成的調用如 FinishMonsterBuffer 也會添加標識符。若是你已經指定了一個標識符並但願生成一個沒有標識符的緩衝區,你能夠經過直接顯示調用FlatBufferBuilder :: Finish 來完成這一目的。
加載緩衝區數據之後,可使用像 MonsterBufferHasIdentifier 這樣的調用來檢查標識符是否存在。
給文件添加標識符是最佳實踐。若是隻是簡單的想經過網絡發送一組可能的消息中的一個,那麼最好用 Union。
默認狀況下,flatc 會將二進制文件輸出爲 .bin
。schema 中的這個聲明會將其改變爲任何你想要的:
file_extension "ext";
複製代碼
RPC 聲明瞭一組函數,它將 FlatBuffer 做爲入參(request)並返回一個 FlatBuffer 做爲 response(它們都必須是 table 類型):
rpc_service MonsterStorage {
Store(Monster):StoreResponse;
Retrieve(MonsterId):Monster;
}
複製代碼
這些產生的代碼以及它的使用方式取決於使用的語言和 RPC 系統,能夠經過增長 --grpc
編譯參數,代碼生成器會對 GRPC 有初步的支持。
Attributes 能夠附加到字段聲明,放在字段後面或者 table/struct/enum/union 的名稱以後。這些字段可能有值也有可能沒有值。
一些 Attributes 只能被編譯器識別,好比 deprecated。用戶也能夠定義一些 Attributes,可是須要提早進行 Attributes 聲明。聲明之後能夠在運行時解析 schema 的時候進行查詢。這個對於開發一個屬於本身的代碼編譯/生成器來講是很是有用的。或者是想添加一些特殊信息(一些幫助信息等等)到本身的 FlatBuffers 工具之中。
目前最新版能識別到的 Attributes 有 11 種。
id:n
(on a table field)deprecated
(on a field)required
(on a non-scalar table field)force_align: size
(on a struct)bit_flags
(on an enum)nested_flatbuffer: "table_name"
(on a field)flexbuffer
(on a field)key
(on a field)hash
(on a field)original_order
(on a table)native_inline
、native_default
、native_custom_alloc
、native_type
、native_include: "path"
。FlatBuffers 是一個高效的數據格式,但要實現效率,您須要一個高效的 schema。如何表示具備徹底不一樣 size 大小特徵的數據一般有多種選擇。
因爲 FlatBuffers 的靈活性和可擴展性,將任何類型的數據表示爲字典(如在 JSON 中)是很是廣泛的作法。儘管能夠在 FlatBuffers(做爲具備鍵和值的表的數組)中模擬這一點,但這對於像 FlatBuffers 這樣的強類型系統來講,這樣作是一種低效的方式,會致使生成相對較大的二進制文件。在大多數系統中,FlatBuffer table 比 classes/structs 更靈活,由於 table 在處理 field 數量很是多,可是實際使用只有其中少數幾個 field 這種狀況,效率依舊很是高。所以,組織數據應該儘量的組織成 table 的形式。
一樣,若是可能的話,儘可能使用枚舉的形式代替字符串。
FlatBuffers 中沒有繼承的概念,因此想表示一組相關數據結構的方式是 union。可是,union 確實有成本,另一種高效的作法就是創建一個 table 。若是這些數據結構有不少類似或者能夠共享的 field ,那麼建議一個 table 是很是高效的。在這個 table 中包含全部數據結構的全部字段便可。高效的緣由就是 optional 字段是很是廉價的,消耗少。
FlatBuffers 默承認以支持存放的下全部整數,所以儘可能選擇所需的最小大小,而不是默認爲 int/long。
能夠考慮用 buffer 中一個字符串或者 table 來共享一些公共的數據,這樣作會提升效率,所以將重複的數據拆成共享數據結構 + 私有數據結構,這樣作是很是值得的。
FlatBuffers 是支持解析 JSON 成本身的格式的。即解析 schema 的解析器一樣能夠解析符合 schema 規則的 JSON 對象。因此和其餘的 JSON 解析器不一樣,這個解析器是強類型的,而且解析結果也只是 FlatBuffers。具體作法請參照 flatc 文檔和 C++ 對應的 FlatBuffers 文檔,查看如何在運行時解析 JSON 成 FlatBuffers。
爲了解析 JSON,除了須要定義一個 schema 之外,FlatBuffers 的解析器還有如下這些改變:
strict_json
標誌輸出它們。foo_type:FooOne
,FooOne 就是能夠在 union 以外使用的 table。解析JSON時,解析器識別字符串中的如下轉義碼:
\n
- 換行。
\t
- 標籤。
\r
- 回車。
\b
- 退格。
\f
- 換頁。
\「
- 雙引號。
\\
- 反斜槓。
\/
- 正斜槓。
\uXXXX
- 16位 unicode,轉換爲等效的 UTF-8 表示。
\xXX
- 8 位二進制十六進制數字 XX。這是惟一一個不屬於 JSON 規範的地方(請參閱json.org/),可是須要可以將字符串中的任意二進制編碼爲文本並返回而不丟失信息(例如字節 0xFF 就不能夠表示爲標準的 JSON)。
當從二進制再反向表示生成 JSON 時,它還會再次生成這些轉義代碼。
schema 中的標識符是爲了翻譯成許多不一樣的編程語言,因此把 schema 的編碼風格改爲和當前項目語言使用的風格,是一種錯誤的作法。應該讓 schema 的代碼風格更加通用。
還有 2 條關於書寫格式的建議:
:
兩邊沒有空格,=
兩邊各一個空格。
大多數可序列化格式(例如 JSON 或 Protocol Buffers)對於某個字段是否存在於某個對象中是很是明確,能夠將其用做「額外」信息。
可是在 FlatBuffers 中,除了標量值以外,這也適用於其餘全部內容。 FlatBuffers 默認狀況下不會寫入等於默認值的字段(對於標量),這樣能夠節省大量空間。 然而,這也意味着測試一個字段是否「存在」有點沒有意義,由於它不會告訴你,該字段是不是經過調用add_field 方法調來 set 的,除非你對非默認值的信息感興趣。默認值是不會寫入到 buffer 中的。
可變的 FlatBufferBuilder 實現了一個名爲 force_defaults 的方法,能夠避免這種行爲,由於即便與默認值相等,也會寫入字段。而後可使用 IsFieldPresent 來查詢 buffer 中是否存在某個字段。
另外一種方法是將標量字段包裝在 struct 中。這樣,若是它不存在,它將返回 null。這種方法厲害的是,struct 不會佔用比它們所表明的標量更多的空間。
讀完本篇 FlatBuffers 編碼原理之後,讀者應該能明白如下幾點:
與 protocol buffers 相比,FlatBuffers 的數據結構定義文件,功能上有如下一些「改進」:
.proto
中擴展一個對象,須要在數字中尋找一個空閒的空位(由於 protocol buffers 有更緊湊的表示方式,因此必須選擇更小的數字)。除了這點不方便以外,它還使得刪除字段成爲問題:若是保留它們,從語意表達上不是很明顯的表達出這個字段不能讀寫了,保留它們,還會生成訪問器。若是刪除它們,就會有出現嚴重 bug 的風險,由於當有人重用了這些 ID,會致使讀取到舊的數據,這樣數據會發生錯亂。除去功能上的不一樣,再就是一些 schema 語法上的細微不一樣:
關於 schema 全部的語法,能夠參考這個文檔
關於 flatbuffers 編解碼性能相關的,原理分析和源碼分析,將在下篇進行。
Reference:
flatbuffers 官方文檔
Improving Facebook's performance on Android with FlatBuffers
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: halfrost.com/flatbuffers…