FlatBuffer

6 minute read

FlatBuffer

FlatBuffer란 메세지 송/수신에 사용되는 플랫폼 종속성 없이 사용가능한 “직/역직렬화 라이브러리”이다.
FlatBuffer사용 이유

  1. 데이터 송/수신 시 Parsing/Unpacking이 필요 없다.
  2. 메모리 효율성이 높고, 빠른 속도를 보장한다.
  3. 사용하는 데이터타입에 대한 유연성이 존재한다.
  4. 적은 양의 코드로 작성 가능하다.
  5. 사용하기 편리하다.
  6. 여러 플랫폼에서 사용가능하다.

위와 같은 장점이 존재하는 FlatBuffer는 직/역직렬화 라이브러리라는 점에서 Google Protocol Buffer 와 같은 역활을 한다.

아래 사진을 참고하면 FlatBuffer를 사용하는 이유를 알 수 있다.


Google Protocol BufferFlatBuffer의 가장 큰 차이점은 FlatBuffer의 사용 이유의 1번의 내용처럼 데이터 송/수신 시 Parsing/Unpacking이 필요 없다는 점 이다.(Zero-copy)
이로 인하여 시간의 측면에서 매우 큰 장점을 나타내는 것을 보인다.

아래 사진또한 FlatBuffer를 사용하는 이유를 알 수 있다.


Google Protocol Buffer에 비해 FlatBuffer의 가장 뚜렷한 장점 2가지는 zero-copy와 Random-access-reads이다.
1) Zero Copy
Zero Copy는 Network에서 Read/Write할때 걸리는 불필요한 Copy과정을 최소화 하자는 것이다.
아래 그림을 살펴보게 되면 기존의 데이터 복사 과정이다.


Application buffer를 거쳐가므로 4번의 과정이 필요하다는 것을 알 수 있다.

아래 그림은 Zero Copy의 과정이다.


Application buffer를 거쳐지 않으므로 2번의 과정이라는 것을 알 수 있다.

2) Random-access-reads
FileBuffers는 각 레코드에 모든 필드 위치에 대한 오프셋 테이블을 저장하고 개체간 포인터를 사용하여 임의 액세스를 허용

구조 및 사용 방법

Google Protocol Buffer 와 마찬가지로 과정이 진행된다.
IDL(Interface Description Language)작성 -> flatc로서 Compile -> 사용


IDL(Interface Description Language) 작성

아래와 같은 예시를 살펴보고 이것에 대한 내용을 정리한다.

// Example IDL file for our monster's schema.

namespace MyGame.Sample;

enum Color:byte { Red = 0, Green, Blue = 2 }

union Equipment { Weapon } // Optionally add more tables.

struct Vec3 {
  x:float;
  y:float;
  z:float;
}

table Monster {
  pos:Vec3;
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated);
  inventory:[ubyte];
  color:Color = Blue;
  weapons:[Weapon];
  equipped:Equipment;
  path:[Vec3];
}

table Weapon {
  name:string;
  damage:short;
}

root_type Monster;


1) Table: Field의 집합
Field 추가 가능
deprecated로서 사용하지 않는 Field정의 가능
Field이름과 Table이름 바꾸기 가능
값을 직접 대입한 뒤, Value값을 주지 않으면 Default Value로서 자동으로 들어가게 된다.

2) Struct: Field의 집합
Field가 추가 되거나 deprecated할 수 없다.
Default Value가 없기때문에 값을 반드시 대입하여야 한다.
Table에 비해 메모리를 적게 차지하고 Access가 빠르다.

3) Type
Scalar

  • 8 bit: byte (int8), ubyte (uint8), bool
  • 16 bit: short (int16), ushort (uint16)
  • 32 bit: int (int32), uint (uint32), float (float32)
  • 64 bit: long (int64), ulong (uint64), double (float64)

Non Scalar

  • Vector: [type]
  • String: [byte] or [ubyte]
  • tables, structs, enums, unions

Filed 변경시 같은 크기의 Type으로만 변경 가능

4) (Default) Values
숫자로 구성된 값
Scalar만 (Default)Values를 가질 수 있고, Non Scalar는 불가능

5) Enums
주어진 값을 가지거나 이전 값에서 하나씩 증가하는 값을 가진 sequence

6) Unions
여러 Message유형을 보낼 때 사용
Table의 이름으로서 구성

7) Namespaces
식별자가 고유하도록 ㄱ보장하는 코드 영역 정의

8) Root type
직렬화 된 데이터의 Root Table


참조: IDL 작성 자세한 내용

flatc

.proto file을 해당 언어에 맞는 클래스로 Compile하는 과정
1) flatbuffers 설치: Download the Package
2)Building with Cmake: Building
3) flatc 실행:
flatc [ GENERATOR OPTIONS ] [ -o PATH ] [ -I PATH ] [ -S ] FILES... [ -- FILES...]

GENERATOR OPTIONS을 활용하여 해당 언어어 맞는 클래스로서 Compile가능
참조: Detail of Compiler Options

실제 동작 Code
flatc --python -o ./ ./monster.fbs
실행 결과(monster.py)


Name Space에 지정한 MyGame/Sample 폴더 안에 .py파일 생성

Writing a Message

1) FileBuffer생성
2) String 존재한다면 CreateString으로 문자열 생성하고 offset을 저장
3) Start(builder)
4) Add(builder, value)
5) result = End(builder)
6) builder.Finish(result)
7) builder.Output()

아래 Code는 FlatBuffer Class를 사용하여서 Monster를 등록하는 과정이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import sys
import flatbuffers
import MyGame.Sample.Color
import MyGame.Sample.Equipment
import MyGame.Sample.Monster
import MyGame.Sample.Vec3
import MyGame.Sample.Weapon

def main():
  builder = flatbuffers.Builder(0)

#Add_weapon
  input_weapons = []
  output_weapons = []

  while True:
    weapon = raw_input("Enter a weapon name,dmage (or leave blank to finish): ")
    if weapon == "":
      break
    name, damage = weapon.split(',')
    imsi = [name, damage]
    input_weapons.append(imsi)
    
  for w in input_weapons:
    weapon_imsi = builder.CreateString(w[0])
   
    MyGame.Sample.Weapon.WeaponStart(builder)
    MyGame.Sample.Weapon.WeaponAddName(builder, weapon_imsi)
    MyGame.Sample.Weapon.WeaponAddDamage(builder, int(w[1]))
    output_weapons.append(MyGame.Sample.Weapon.WeaponEnd(builder))
    
  MyGame.Sample.Monster.MonsterStartWeaponsVector(builder, len(output_weapons))
  
  for o in output_weapons:
    builder.PrependUOffsetTRelative(o)
  # Note: Since we prepend the data, prepend the weapons in reverse order.
  weapons = builder.EndVector(len(input_weapons))

 # Add_name
  input_name = raw_input("Enter monster name: ")
  name = builder.CreateString(name)


 # Add-Inventory
  input_inventory = int(raw_input("Enter a inventory: "))
  MyGame.Sample.Monster.MonsterStartInventoryVector(builder, input_inventory)
  # Note: Since we prepend the bytes, this loop iterates in reverse order.
  for i in reversed(range(0, 10)):
    builder.PrependByte(i)
  inv = builder.EndVector(10)

 # Add-pos
  input_pos = raw_input("Enter monster pos(x,y,z): ")
  x,y,z = input_pos.split(',')
  pos = MyGame.Sample.Vec3.CreateVec3(builder, float(x), float(y), float(z))

 #Add-Hp
  input_hp = int(raw_input("Enter a hp: "))

 #Add-Color
  input_color = raw_input("Is this Monster'color Red, Green, or Blue? ")
  

 # Add-Mana
  input_mana = int(raw_input("Enter a Mana: "))


  MyGame.Sample.Monster.MonsterStart(builder)
  MyGame.Sample.Monster.MonsterAddPos(builder, pos)
  MyGame.Sample.Monster.MonsterAddMana(builder, input_mana)
  MyGame.Sample.Monster.MonsterAddHp(builder, input_hp)
  MyGame.Sample.Monster.MonsterAddName(builder, name)
  MyGame.Sample.Monster.MonsterAddWeapons(builder, weapons)
  MyGame.Sample.Monster.MonsterAddInventory(builder, inv)

  
  if input_color == "Red":
    MyGame.Sample.Monster.MonsterAddColor(builder,MyGame.Sample.Color.Color().Red)
  elif input_color == "Green":
    MyGame.Sample.Monster.MonsterAddColor(builder,MyGame.Sample.Color.Color().Green)
  elif input_color == "Blue":
    MyGame.Sample.Monster.MonsterAddColor(builder,MyGame.Sample.Color.Color().Blue)
  else:
    print("Unknown Color type; leaving as default value.(Red)")
    MyGame.Sample.Monster.MonsterAddColor(builder,MyGame.Sample.Color.Color().Red)
  
  
  MyGame.Sample.Monster.MonsterAddEquippedType(
      builder, MyGame.Sample.Equipment.Equipment().Weapon)
  print("Which weapon",input_name," equipped?")
  count = 0
  for w in input_weapons:
    print("Number: ",count,'Weapon_name: ',w[0])
    count = count+1

  select_weapon = int(raw_input("Choose Select: "))
  
  MyGame.Sample.Monster.MonsterAddEquipped(builder, output_weapons[select_weapon])
  

  orc = MyGame.Sample.Monster.MonsterEnd(builder)

  builder.Finish(orc)

  buf = builder.Output()

  with open(sys.argv[1], "wb") as f:
    f.write(buf)
  print 'The FlatBuffer was successfully created and verified!'

if __name__ == '__main__':
  main()


실제 Input값

Variable실제값
namemonster1
mana10
hp20
ColorRed
Pos10, 20, 30
weapons
  • name: w1, damage: 10
  • name: w2, damage: 20
inventory 10
Equippedname: w1, damage: 10


실행 결과


실행 결과 Binary File로서 저장되는 것을 알 수 있다.

Reading a Message

Non-deprecated Scalar 변수에 대해서는 바로 접근 가능하다.

1
2
3
hp = monster.Hp()
mana = monster.Mana()
name = monster.Name()


Sub-Object의 경우 Sub-Object에 접근 한 뒤 Sub-Object 의 Field에 접근 가능

1
2
3
4
pos = monster.Pos()
x = pos.X()
y = pos.Y()
z = pos.Z()


Vector의 경우 Length를 통하여 길이를 알아낸 뒤 반복문으로 접근 가능

1
2
inv_len = monster.InventoryLength()
third_item = monster.Inventory(2)


Union의 경우 해당 위치와 크기를 통하여 접근 가능하다.

1
2
3
4
5
6
7
8
union_type = monster.EquippedType()
if union_type == MyGame.Sample.Equipment.Equipment().Weapon:
  # `monster.Equipped()` returns a `flatbuffers.Table`, which can be used to
  # initialize a `MyGame.Sample.Weapon.Weapon()`.
  union_weapon = MyGame.Sample.Weapon.Weapon()
  union_weapon.Init(monster.Equipped().Bytes, monster.Equipped().Pos)
  weapon_name = union_weapon.Name()
  weapon_damage = union_weapon.Damage()


위의 Union의 접근을 통해서 아래와 같은 그림을 예상할 수 있다.


즉, FileBuffers는 offset이 어디서부터 시작되고 끝나는지를 명시함으로써 Random-acess-read가 가능하다는 것을 알 수 있다.
아래 코드는 만들어진 Binary File을 읽는 과정이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python
# coding: utf-8


import monster_pb2
import sys


def ListMonster(address_monster):
  for monster in address_monster.monster:
    print("Monster pos: ",monster.pos.x, monster.pos.y, monster.pos.z)
    print("Monster mana:", monster.mana)
    print("Monster hp:", monster.hp)
    print("Monster name:", monster.name)
    print("Monster friendly:", monster.friendly)
    
    print("Monster inventory: ")
    for monster_inventory in monster.inventory:
        print(monster_inventory)
    
    print("Monster color: ",monster.color)
    
    print("Monster weapon: ")
    for w in monster.weapons:
        print("Name: ",w.name)
        print("Damage: ",w.damage)
    
    print("Monster equipped")
    for e in monster.equipped.weapon:
        print(e)
    
    print("Monster path:")
    for p in monster.path:
        print(p)


if len(sys.argv) != 2:
  print("Usage:", sys.argv[0], "Save_FILE")
  sys.exit(-1)

address_monster = monster_pb2.AddressMonster()


with open(sys.argv[1], "rb") as f:
  address_monster.ParseFromString(f.read())

ListMonster(address_monster)


실행 결과

Monster Weapon_list
Weapon Name:  b'w2'
Weapon Damage:  20
Weapon Name:  b'w1'
Weapon Damage:  10
Monster Name b'w2'
Monster Inventory
0 1 2 3 4 5 6 7 8 9 
Monster Pos
Pos.x:  10.0
Pos.y:  20.0
Pos.z:  30.0
Monster Hp 20
Monter Color: Red
Monster Mana 10
Monster Equipped List
Weapon name:  b'w1'
Weapon damage:  10


참조:Writing & Reading A Message 자세한 내용



참조: 원본코드
참조: 곰팡이 먼지연구소
참조: capnproto
참조: skydays 블로그
문제가 있거나 궁금한 점이 있으면 wjddyd66@naver.com으로 Mail을 남겨주세요.

Categories:

Updated:

Leave a comment