在 Java 中使用 FlatBuffers 進行序列化
一、簡介
在本教程中,我們將探索 Java 中的FlatBuffers並使用它來執行序列化和反序列化。
2.Java中的序列化
序列化是將 Java 物件轉換為可以透過網路傳輸或保存在檔案中的位元組流的過程。 Java 透過java.io.Serializable
介面以及java.io.ObjectOutputStream
和java.io.ObjectInputStream
類別提供內建的物件序列化機制。
然而,由於它有幾個缺點,包括處理複雜物件圖和依賴類別的複雜方法,因此有幾個程式庫可用於 Java 中的序列化和反序列化。
一些廣泛使用的 Java 序列化函式庫包括 Jackson 和 Gson。物件序列化格式的更新標準是協定緩衝區。 Protocol Buffers 是由 Google 開發的一種與語言無關的二進位序列化格式。它們用於效率和互通性至關重要的高效能環境和分散式系統。
3.FlatBuffers
FlatBuffers是Google開發的一個高效率的跨平台序列化函式庫。它支援多種語言,例如 C、C++、Java、Kotlin 和 Go。 FlatBuffers 是為遊戲開發而創建的;因此,效能和低記憶體開銷是其設計中的預設考慮因素。
FlatBuffers 和 Protocol Buffers 由 Google 創建,是非常相似的基於二進位的資料格式。這兩種格式都支援高效的高速序列化和反序列化。主要區別在於 FlatBuffers 在存取之前不需要將額外的資料解包到中間資料結構。
3.1. FlatBuffers 庫簡介
完整的 FlatBuffers 實作由以下元件組成:
- FlatBuffer 架構文件
- 一個
flatc
編譯器 - 序列化和反序列化程式碼
FlatBuffer 模式檔案可作為我們將使用的資料模型結構的範本。架構檔案的語法遵循與 C 類型或其他介面描述語言 (IDL) 格式類似的模式。我們需要定義模式和flatc
編譯器,然後編譯模式檔。
3.2.表和模式
FlatBuffer 是一個二元緩衝區,其中包含使用偏移量組織的巢狀物件(例如結構、表格和向量)。
這種安排允許就地遍歷數據,類似於傳統的基於指標的資料結構。然而,與許多記憶體中資料結構不同,FlatBuffers 嚴格遵守對齊和位元組序規則(總是小),確保跨平台相容性。此外,對於表對象,FlatBuffers 提供向前和向後相容性。
FlatBuffers 中的表格是最基本的資料結構,用於表示具有命名欄位的複雜結構。表類似於某些語言中的類別或結構,支援多種類型的字段,例如 int、short、string、struct、vector,甚至其他表。
3.3. flatc
編譯器
flatc
編譯器是 FlatBuffers 提供的重要工具,它可以產生各種程式語言(例如 C++ 和 Java)的程式碼,以幫助根據模式序列化和反序列化資料。此編譯器輸入模式定義並以所需的程式語言產生程式碼。
在接下來的部分中,我們將編譯架構檔案以產生程式碼。然而,我們需要先建置和設定我們的編譯器才能使用它。
我們首先將flatbuffers
庫克隆到我們的系統中:
$ git clone https://github.com/google/flatbuffers.git
建立flatbuffers
目錄後,我們使用cmake
將庫建置為可執行檔。 CMake(跨平台Make)是一個開源的、獨立於平台的建置系統,旨在自動化建構軟體專案的過程:
$ cd flatbuffers
$ mkdir build
$ cd build
$ cmake ..
這樣就完成了flatc
編譯器的建置過程。我們可以透過列印版本來驗證安裝是否成功:
$ ./flatc --version
flatc version 23.5.26
編譯後的檔案現在儲存在/flatbuffers/build
路徑下, flatc
可執行檔也可以在同一目錄中使用。我們將使用此文件來建立所有架構文件,因此,我們可以建立此路徑的捷徑或別名。
4. 使用 FlatBuffers
在本節中,我們將透過實作我們的用例來探索 FlatBuffers 庫。假設我們正在開發一款跨越海洋、山地、平原等不同地形的遊戲。每個地形都有自己的一組獨特的屬性。
地形資訊是載入遊戲關卡所必需的,並且需要透過網路傳輸給玩家。高效率的序列化和反序列化是必須的。
4.1.模式定義
我們應該開始的第一件事是定義我們的地形模式類型。地形是我們的flatbuffer
中的一張桌子。它可以有許多屬性,例如名稱(海洋、陸地、山脈、沙漠等)、顏色和位置(以 3D 向量座標的形式)。地形也會產生影響。例如,沙漠中可能發生沙塵暴,陸地上可能發生洪水。效果可以是原始模式中的單獨表。
有了這樣的理解,我們就可以將模式寫成如下:
namespace MyGame.baeldung;
enum Color:byte { Brown = 0, Red = 1, Green = 2, Blue = 3, White = 4 }
struct Vec3 {
x:float;
y:float;
z:float;
}
table Terrain {
pos:Vec3; // Struct.
name:string;
color:Color = Blue;
navigation: string;
effects: [Effect]
}
table Effect {
name:string;
damage:short;
}
root_type Terrain;
我們有一個用於識別地形顏色的枚舉,一個用於座標的結構體,以及兩個表,Terrain 和 Effect,其中 Terrain 是根類型。
4.2.架構編譯
flatc
編譯器已準備就緒,我們用它來編譯我們的模式檔terrain.fbs
:
$ cd <path to schema>
$ flatc --java terrain.fbs
我們應該注意,根據上一節中所述的安裝位置,不同系統的flatc
路徑可能會有所不同。
4.3.創建物件並執行序列化
架構已經編譯完畢並準備就緒。我們可以開始使用該模式為我們的遊戲創建一些地形。作為本範例演練的一部分,我們將為我們的地形創建一個沙漠地形和一些效果。
要在Java中使用FlatBuffers,我們需要加入Maven依賴:
<dependency>
<groupId>com.google.flatbuffers</groupId>
<artifactId>flatbuffers-java</artifactId>
<version>23.5.26</version>
</dependency>
我們現在可以匯入flatbuffers
庫以及從我們的架構中產生的文件:
import MyGame.terrains.*;
import com.google.flatbuffers.FlatBufferBuilder;
作為編譯過程的一部分產生的檔案位於架構的namespace
部分中定義的相同路徑下(在我們的範例中為MyGame
)。
編譯後會出現一個Effect
類別供我們使用,它提供了createEffect()
方法。我們將用它來創建我們想要的效果。我們首先建立一個初始緩衝區大小為 1024 位元組的建構器物件:
FlatBufferBuilder builder = new FlatBufferBuilder(INITIAL_BUFFER);
int sandstormOffset = builder.createString("sandstorm");
short damage = 3;
int sandStorm = MyGame.terrains.Effect.createEffect(builder, sandstormOffset, damage);
我們可以用同樣的方式添加更多效果。
接下來,我們創建沙漠地形。讓我們為地形指定顏色,並為其指定名稱和導航位置:
byte color = MyGame.terrains.Color.YELLOW;
int terrainName = builder.createString("Desert");
int navigationName = builder.createString("south");
我們使用Terrain
類別自動產生的靜態方法來新增更多地形元資料和效果:
int effectOffset = MyGame.terrains.Terrain.createEffectsVector(builder, effects);
startTerrain(builder);
addName(builder, terrainName);
addColor(builder, color);
addNavigation(builder, navigationName);
addEffects(builder, effectOffset);
int desert = endTerrain(builder);
builder.finish(desert);
現在讓我們在flatbuffer
中序列化地形及其效果。我們可以儲存緩衝區或透過網路將其傳輸到客戶端:
ByteBuffer buf = builder.dataBuffer();
4.4.使用 FlatBuffer 進行反序列化
讓我們反序列化flatbuffer
物件並訪問地形。我們將從緩衝區創建的序列化位元組數組開始,並將其轉換為ByteBuffer
緩衝區:
ByteBuffer buf = java.nio.ByteBuffer.wrap(buffer);
這允許我們從緩衝區獲取根Terrain
物件的存取器並存取其所有屬性:
Terrain myTerrain = Terrain.getRootAsTerrain(buf)
Assert.assertEquals(terrain.name(), "Desert");
Assert.assertEquals(terrain.navigation(), "south");
編譯器產生的程式碼顯示實體的每個屬性都帶有關聯的存取器。我們還可以存取相關的效果:
Effect effect1 = terrain.effectsVector().get(0);
Effect effect2 = terrain.effectsVector().get(2);
Assert.assertEquals(effect1.name(), "Sandstorm");
Assert.assertEquals(effect2.name(), "Drought");
4.5.改變 FlatBuffers
FlatBuffers 由於其靜態模板結構而大多是唯讀的。然而,我們可能會遇到這樣的情況:我們需要在將flatbuffer
中的某些內容傳送到另一段程式碼之前對其進行更改。假設我們想要將沙塵暴效果的傷害分數從現有值 3 更新為 10。
在這種情況下,平面flatbuffers
的就地突變會派上用場。
只有當我們使用–gen-mutable
參數建構模式時,才可能發生flatbuffer
的突變:
$ ./../flatc --gen-mutable --java terrain.fbs
這為我們提供了所有存取器上的mutate()
方法,我們可以使用它來修改flatbuffer
的值:
Assert.assertEquals(effect1.damage(), 3);
effect1.mutateDamage((short) 10);
Assert.assertEquals(effect1.damage(), 10);
5. 使用 FlatBuffers 進行 JSON 轉換
flatc
編譯器提供了將二進位轉換為 JSON 的技術,反之亦然。假設我們有一個地形的 JSON 檔案。我們可以使用編譯器使用以下程式碼從 JSON 檔案建立二進位檔案:
flatc --binary <template file> <json file>
$ flatc --binary terrain.fbs sample_terrain.json
相反,我們也可以將二進位檔案轉換為完整的 JSON 檔案:
flatc --json --raw-binary <template file> -- <binary file>
$ flatc --json --raw-binary terrain.fbs -- sample_terrain.bin
6. 使用 FlatBuffers 的好處
使用這個跨平台序列化函式庫有很多好處:
- FlatBuffers 在平面二進位緩衝區中組織分層數據,我們可以直接存取它,而無需解析或解包的開銷
- 對我們資料結構的增量變更會自動且乾淨地合併,從而可以輕鬆保持與我們不斷發展的模型的向後相容性
- 它們在記憶體使用率方面也很高效,因為我們只需要緩衝區佔用的記憶體空間來存取您的數據
- 他們留下了很小的程式碼足跡。產生的程式碼很少,我們只需要一個小標頭作為依賴項,讓整合變得輕而易舉
- FlatBuffers 是強型的;因此,我們可以在編譯時捕獲錯誤
七、結論
在本文中,我們探討了 FlatBuffers 函式庫及其序列化和反序列化複雜資料的功能。我們採用了實踐方法來使用該程式庫實作程式碼,並研究了flatbuffers
的優點和用例。
與往常一樣,程式碼可以在 GitHub 上取得。