Java中使用FlatBuffer实现序列化

在本教程中,我们将探索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; // Struct.
  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("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();

反序列化
让我们反序列化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");

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