FlatBuffers源码解析

本文深入解析谷歌的FlatBuffers,一种基于二进制数据的序列化库,以提高数据传输效率。相较于JSON,FlatBuffers通过偏移量直接访问数据,减少了解析时的时间和内存开销。文章通过源码分析创建FlatBufferBuilder、字符串存储、table构建过程,并以示例说明其工作原理,展示了如何通过偏移量实现快速数据访问和高效的内存使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在观看谷歌发布的视频时,发现视频将json或者xml成为网络传输中的horrible type。这让天天和json打交道的我感觉很疑惑,后面提到了谷歌的FlatBuffers,仿佛打开新世界的大门。虽然我现在开发的业务json的解析并不是瓶颈,但是多学点东西总不是坏事,本篇文章就从源码角度分析下flatbuffer的原理,相信大家看完也就知道flatbuffer快在哪和慢在哪了。

首先我们知道json是基于字符串和各种特殊字符进行解析的,这也就意味着我们在解析的时候需要将数据先整理成标准的json格式,这其中的内存和时间开销较大。而flatbuffers是一种基于二进制数据,根据偏移量进行解析。也就是说省略了解析过程中的各种时间和内存损耗,映衬了名字中的flat。

本文从官方给出的示例结合源码进行分析,由于源码较少,大家可以下载后编进自己项目中即可。

首先我们看,创建一个FlatBufferBuilder做了什么。

    public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory,
                             ByteBuffer existing_bb, Utf8 utf8) {
        if (initial_size <= 0) {
          initial_size = DEFAULT_BUFFER_SIZE;
        }
        // 这里的bb_factory是一个单例,用于申请bytebuffer的空间
        this.bb_factory = bb_factory;
        if (existing_bb != null) {
          bb = existing_bb;
          bb.clear();
          bb.order(ByteOrder.LITTLE_ENDIAN);
        } else {
          // 这里申请了缓冲区
          bb = bb_factory.newByteBuffer(initial_size);
        }
        this.utf8 = utf8;
        // 由于基于偏移量进行存储和解析,需要时刻知道缓冲区大小以及被占用的大小
        space = bb.capacity();
    }

这时候我们拿到了builder对象,之后对于数据的存放均基于这个对象。也就是放到我们申请的缓冲区中。

我们以builder.createString()为例

    public int createString(CharSequence s) {
        // flatbuffers支持的字符串均为UTF-8编码,对于字符串的处理的性能低
        // 也是因为这里有解码转码的过程
        int length = utf8.encodedLength(s);
        // 插入一个类似于分隔符的东西,具体操作就是加入缓冲区,更新下大小和space数据
        addByte((byte)0);
        // 进行空间的申请
        startVector(1, length, 1);
        bb.position(space -= length);
        // 将数据放入缓冲区
        utf8.encodeUtf8(s, bb);
        // 最后把当前长度放到缓冲区末尾,返回缓冲区目前位置
        return endVector();
    }

放入字符串相对于是较为复杂的操作,其他的createInt()等基本数据类型,操作无外乎申请空间-放入数据-更新并返回最新位置。这些返回的位置需要进行保存,之后构建对象时会有用。

注意,flatbuffers相对麻烦的一点就是,字符串放入后无法进行更改,而且创建之后数据就存在缓冲区中了。对于基本数据类型可以进行更改,因为他们占用的空间是相同的,不会影响缓冲区的位置。

下面我们看一下table相关代码,table是flatbuffers最重要的组成,看schema可以看出table类似于一个对象,也和json中一个json对象相似。

首先,相关代码是根据schema自动生成的,官网示例schema如下:

// Example IDL file for our monster's schema.
namespace com.example.myapplication.autocode;
enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment { Weapon,Person } // Optionally add more tables.
struct Vec3 {
  x:float;
  y:float;
  z:float;
}
table Monster {
  pos:Vec3; // Struct.
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated);
  inventory:[ubyte];  // Vector of scalars.
  color:Color = Blue; // Enum.
  weapons:[Weapon];   // Vector of tables.
  equipped:Equipment; // Union.
}
table Person {
    name:string;
    age:short;
}
table Weapon {
  name:string;
  damage:short;
}
root_type Monster;

table中可以持有数组、基本数据类型、枚举类、其他table,完全可以实现和json相同的功能。

通过startTable(int numfileds)方法进行对象构建,其中numfileds表示参数的个数,比如上述的table Monster中有十个参数,因为union Equipment { Weapon,Person }中有两个table。

我们看看startTable干了什么

    public void startTable(int numfields) {
        notNested();
        // 简单来说就是把当前使用的vtable变成我们正在构建的对象
        if (vtable == null || vtable.length < numfields) vtable = new int[numfields];
        vtable_in_use = numfields;
        Arrays.fill(vtable, 0, vtable_in_use, 0);
        nested = true;
        object_start = offset();
    }

由于数据已经准备完毕,之后无论构建什么数据,所做的就是三件事:准备空间-将该变量的偏移放入缓冲区-更新vtable中该属性的偏移量。

最后结束构建,执行endTable()

    public int endTable() {
        if (vtable == null || !nested)
            throw new AssertionError("FlatBuffers: endTable called without startTable");
        // 加个0当分隔符,这个在不同元素中也会进行添加
        addInt(0);
        int vtableloc = offset();
        int i = vtable_in_use - 1;
        for (; i >= 0 && vtable[i] == 0; i--) {}
        int trimmed_size = i + 1;   
        // 自后向前,相对偏移量放入缓冲区
        for (; i >= 0 ; i--) {
            // Offset relative to the start of the table.
            short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);
            addShort(off);
        }
        
        // 放入两个固定内容:vtable的大小以及加上两个固定内容所占的大小
        final int standard_fields = 2; // The fields below:
        addShort((short)(vtableloc - object_start));
        addShort((short)((trimmed_size + standard_fields) * SIZEOF_SHORT));

        // Search for an existing vtable that matches the current one.
        int existing_vtable = 0;
        outer_loop:
        for (i = 0; i < num_vtables; i++) {
            int vt1 = bb.capacity() - vtables[i];
            int vt2 = space;
            short len = bb.getShort(vt1);
            if (len == bb.getShort(vt2)) {
                for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
                    if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
                        continue outer_loop;
                    }
                }
                existing_vtable = vtables[i];
                break outer_loop;
            }
        }

        if (existing_vtable != 0) {
            // Found a match:
            // Remove the current vtable.
            space = bb.capacity() - vtableloc;
            // Point table to existing vtable.
            bb.putInt(space, existing_vtable - vtableloc);
        } else {
            // No match:
            // Add the location of the current vtable to the list of vtables.
            if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);
            vtables[num_vtables++] = offset();
            // Point table to current vtable.
            bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);
        }

        nested = false;
        return vtableloc;
    }

一个vtable可以为多个table object提供描述信息,每个table object必须包含一个32位的table offset,指定哪个vtable描述它的字段信息。

每个vtable都有一个16位的vtable size,用来记录本身的大小(包括vtable size);table object size用来记录table object的大小(包括table offset这4个字节),table field offset用来记录table instance内的字段相对table object的偏移量,即table instance的第一个字段偏移量必须为4(因为vtable offset为32位,占了4个字节)。

为了让大家更感性的感知加入的这一堆偏移量,我们输出一下生成的bytebuffer。由于采用小端序存储,我们从后往前看一下,[40]-[43]为“剑”,[36]为长度,同理[28]-[33]为“斧子”,[24]为长度。

在加入“剑”这一属性的时候,“剑”的真实位置和当时的位置偏移为16,[20]记录了16。[18]记录了damage为3。接下来比较难理解,为了一个vtable存储多个table信息,我们在存储完毕所有信息的时候,buffer位于[32]转成小端序也就是[12]这个位置,[12]、[10]记录的8和6表示相对数据区的偏移,[4]和[6]存放的时固定属性,[0]的12表示整个除了数据区的table大小。

在取数的时候,我们先拿到整个vtable的大小,然后进行反向计算,[12]的8表示vtable起点的偏移,然后我们拿到了[4]和[6]的两个固定变量,注意这里,在自动生成类代码的时候,已经规定了属性在vtable中的偏移,通过table的__offset方法可以拿到当时生成的位置,具体方式就是利用vtable的起始位置加上默认的偏移,也就是8和10,注意这里其实屏蔽了我们添加的“斧子”属性,但是这个属性还是会占有空间,注意不要生成一些不需要的属性。之后再利用buffer中的[8]和[10]的位置加上vtable结尾位置12,就能找到我们放置的数据,这里利用类似操作系统中的相对寻址的方式,多次的偏移为的是使得table可以多层嵌套以及vtable的可拓展性,大家可以试着分析一下嵌套情况。不得不说谷歌程序员真的逻辑怪,大家看到这也知道为啥这玩意不易懂了,但是这里的取数据速度可以比json快上百倍,而且没有多余内存的消耗,其实就是在buffer中取数据。时间空间的消耗都很小。

鄙人才疏学浅,如有错误恳请大家指正,谢谢

Flatbuffers是由Google开发的一种高效序列化库,特别适合需要高效数据处理的应用场景。要在Java项目中使用Flatbuffers,首先需要了解其跨平台特性、数据兼容性以及内存效率等优势。这里提供一个详细的集成和使用Flatbuffers的步骤,以帮助您顺利开始使用这一技术。 参考资源链接:[Flatbuffers Java 1.12.0 中文API文档完整资源包](https://wenku.youkuaiyun.com/doc/6cvdppbnw6?spm=1055.2569.3001.10343) 1. 添加Maven依赖:在项目的`pom.xml`文件中,添加Flatbuffers Java库的Maven依赖配置。 ```xml <dependencies> <dependency> <groupId>com.google.flatbuffers</groupId> <artifactId>flatbuffers-java</artifactId> <version>1.12.0</version> </dependency> </dependencies> ``` 2. 导入API文档和源代码:如果需要查阅API文档或者对源代码进行分析,可以使用提供的`flatbuffers-java-1.12.0-javadoc.jar`和`flatbuffers-java-1.12.0-sources.jar`。这将帮助您理解每个API的使用方法和内部实现。 3. 编写schema文件:在您的资源文件夹下创建`.fbs`文件,并定义好需要的数据结构。例如,创建一个简单的用户信息结构: ```fbs table User { id:int; name:string; } ``` 4. 生成Java类:使用`flatc`编译器根据`.fbs`文件生成对应的Java类文件。 ```shell flatc --java your_schema.fbs ``` 5. 编写代码操作数据:在您的Java代码中引入生成的类,并使用Flatbuffers的API进行数据操作。 ```java import com.google.flatbuffers.*; public class FlatbuffersExample { public static void main(String[] args) { // 创建FlatBufferBuilder对象 FlatBufferBuilder builder = new FlatBufferBuilder(0); // 创建数据并构建 int nameOffset = builder.createString( 参考资源链接:[Flatbuffers Java 1.12.0 中文API文档完整资源包](https://wenku.youkuaiyun.com/doc/6cvdppbnw6?spm=1055.2569.3001.10343)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值