Java中使用FlatBuffer實現序列化

banq發表於2024-03-11

在本教程中,我們將探索Java 中的FlatBuffers並使用它執行序列化和反序列化。

Java序列化是將 Java 物件轉換為可以透過網路傳輸或儲存在檔案中的位元組流的過程。Java 透過java.io.Serialized介面以及java.io.ObjectOutputStream和java.io.ObjectInputStream類提供內建的物件序列化機制。

然而,由於它有幾個缺點,包括處理複雜物件圖和依賴類的複雜方法,因此有幾個庫可用於 Java 中的序列化和反序列化:

  • 一些廣泛使用的 Java 序列化庫包括Jackson和Gson。
  • 物件序列化格式的更新標準是Protocol Buffers。Protocol Buffers 是由 Google 開發的一種與語言無關的二進位制序列化格式。它們用於效率和互操作性至關重要的高效能環境和分散式系統。

FlatBuffers
FlatBuffers是Google開發的一個高效的跨平臺序列化庫。它支援多種語言,例如 C、C++、Java、Kotlin 和 Go。FlatBuffers 是為遊戲開發而建立的;因此,效能和低記憶體開銷是其設計中的預設考慮因素。

FlatBuffers 和 Protocol Buffers 由 Google 建立,是非常相似的基於二進位制的資料格式。這兩種格式都支援高效的高速序列化和反序列化。主要區別在於 FlatBuffers 在訪問之前不需要將額外的資料解包到中間資料結構。

FlatBuffers 庫簡介
完整的 FlatBuffers 實現由以下元件組成:

  • FlatBuffer 模式檔案
  • 一個flatc編譯器
  • 序列化和反序列化程式碼

FlatBuffer 模式檔案充當我們將使用的資料模型結構的模板。架構檔案的語法遵循與 C 型別或其他介面描述語言 (IDL) 格式類似的模式。我們需要定義模式和flatc編譯器,然後編譯模式檔案。

FlatBuffer 是一個二進位制緩衝區,其中包含使用偏移量組織的巢狀物件(例如結構、表和向量)。

這種安排允許就地遍歷資料,類似於傳統的基於指標的資料結構。然而,與許多記憶體中資料結構不同,FlatBuffers 嚴格遵守對齊和位元組序規則(總是小),確保跨平臺相容性。此外,對於表物件,FlatBuffers 提供向前和向後相容性。

FlatBuffers 中的表是最基本的資料結構,用於表示具有命名欄位的複雜結構。表類似於某些語言中的類或結構體,支援多種型別的欄位,例如 int、short、string、struct、vector,甚至其他表。

使用 FlatBuffers 的好處
使用這個跨平臺序列化庫有很多好處:

  • FlatBuffers 在平面二進位制緩衝區中組織分層資料,我們可以直接訪問它,而無需解析或解包的開銷
  • 對我們資料結構的增量更改會自動且乾淨地合併,從而可以輕鬆保持與我們不斷髮展的模型的向後相容性
  • 它們在記憶體利用率方面也很高效,因為我們只需要緩衝區佔用的記憶體空間來訪問您的資料
  • 他們留下了很小的程式碼足跡。生成的程式碼很少,我們只需要一個小標頭作為依賴項,使整合變得輕而易舉
  • FlatBuffers 是強型別的;因此,我們可以在編譯時捕獲錯誤


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可執行檔案也可以在同一目錄中使用。我們將使用此檔案來構建所有架構檔案,因此,我們可以建立此路徑的快捷方式或別名。

使用 FlatBuffers
在本節中,我們將透過實現我們的用例來探索 FlatBuffers 庫。假設我們正在開發一款跨越海洋、山地、平原等不同地形的遊戲。每個地形都有自己的一組獨特的屬性。

地形資訊是載入遊戲關卡所必需的,並且需要透過網路傳輸給玩家。高效的序列化和反序列化是必須的。

我們應該開始的第一件事是定義我們的地形結構模式型別。地形是我們的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; <font>// Struct.<i>
  name:string;
  color:Color = Blue;
  navigation: string;
  effects: [Effect]
}
table Effect {
  name:string;
  damage:short;
}
root_type Terrain;

我們有一個用於識別地形顏色的列舉,一個用於座標的結構體,以及兩個表,Terrain 和 Effect,其中 Terrain 是根型別。

flatc編譯器已準備就緒,我們用它來編譯我們的模式檔案terrain.fbs:

$ cd <path to schema>
$ flatc --java terrain.fbs
我們應該注意,根據上一節中描述的安裝位置,不同系統的flatc路徑可能會有所不同。

實現序列化
該架構已經編譯完畢並準備就緒。我們可以開始使用該模式為我們的遊戲建立一些地形。作為本示例演練的一部分,我們將為我們的地形建立一個沙漠地形和一些效果。

要在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;

作為編譯過程的一部分生成的檔案位於架構的名稱空間部分中定義的相同路徑下(在我們的示例中為MyGame)。

編譯後會出現一個Effect類供我們使用,它提供了createEffect()方法。我們將用它來建立我們想要的效果。我們首先建立一個初始緩衝區大小為 1024 位元組的構建器物件:

FlatBufferBuilder builder = new FlatBufferBuilder(INITIAL_BUFFER);
int sandstormOffset = builder.createString(<font>"sandstorm");
short damage = 3;
int sandStorm = MyGame.terrains.Effect.createEffect(builder, sandstormOffset, damage);

我們可以用同樣的方式新增更多效果。

接下來,我們建立沙漠地形。讓我們為地形指定顏色,併為其指定名稱和導航位置:

byte color = MyGame.terrains.Color.YELLOW;
int terrainName = builder.createString(<font>"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();

反序列化
讓我們反序列化Flatbuffer物件並訪問地形。我們將從緩衝區建立的序列化位元組陣列開始,並將其轉換為ByteBuffer緩衝區:

ByteBuffer buf = java.nio.ByteBuffer.wrap(buffer);

這允許我們從緩衝區獲取根Terrain物件的訪問器並訪問其所有屬性:

Terrain myTerrain = Terrain.getRootAsTerrain(buf)
Assert.assertEquals(terrain.name(), <font>"Desert");
Assert.assertEquals(terrain.navigation(),
"south");

編譯器生成的程式碼顯示實體的每個屬性都帶有關聯的訪問器。我們還可以訪問相關的效果:

Effect effect1 = terrain.effectsVector().get(0);
Effect effect2 = terrain.effectsVector().get(2);
Assert.assertEquals(effect1.name(), <font>"Sandstorm");
Assert.assertEquals(effect2.name(),
"Drought");

使用 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
 

相關文章