汎用シリアライズ方法(MessagePack/Protocol Buffers/FlatBuffers)

(2016-12-30)

MessagePackとは

JSONのように使うことができ、速くてサイズが小さい。

{"compact":true,"スキーマ":{"number": 999, "string": "aaa"}}

のjson(61bytes)をMessagePack(45bytes)に変換したのがこれ。見やすいように改行している。

82 
a7 63 6f 6d 70 61 63 74 c3 
ac e3 82 b9 e3 82 ad e3 83 bc e3 83 9e 
   82 
   a6 6e 75 6d 62 65 72 cd 03 e7 
   a6 73 74 72 69 6e 67 a3 61 61 61

一行目の82要素数2のfixmap(要素数15まで)を表す。

二行目のa77バイトのfixstr(31bytesまで)で、 63 6f 6d 70 61 63 74が”compact”。c3true

三行目のacは12バイトのfixstrで、e3 82 b9 e3 82 ad e3 83 bc e3 83 9eが”スキーマ”。

四行目はこれのvalueで、要素数2のfixmap(82)。

五行目は6バイトのfixstr(a6)、”number”(6e 75 6d 62 65 72)、 cduint16で、999(03 e7)。

六行目は6バイトのfixstr(a6)、”string”(73 74 72 69 6e 67)、3バイトのfixstr(a3)、”aaa”(61 61 61)。

と、こんな感じで分かりやすいバイナリになっている。文字列が多いとあまりサイズが変わらない。

使い方

$ go get github.com/ugorji/go/codec
mh := codec.MsgpackHandle{RawToString: true}
origin := serialize.Data{
	Title: "aaaa",
}

func MessagePackEncode(mh codec.MsgpackHandle, origin Data) []byte {
	var encoded []byte

	enc := codec.NewEncoderBytes(&encoded, &mh)
	if err := enc.Encode(&origin); err != nil {
		panic(err)
	}

	return encoded
}

func MessagePackDecode(mh codec.MsgpackHandle, bytes []byte) Data {
	var decoded Data

	dec := codec.NewDecoderBytes(bytes, &mh)
	if err := dec.Decode(&decoded); err != nil {
		panic(err)
	}

	return decoded
}

Protocol Buffersとは

Googleのシリアライズライブラリ。gRPCでも使われる。

Googleが作ったRPCフレームワークgRPCを使ってみた

message typeをprotoファイルで定義する(proto3)。このファイルから各言語のコードを生成することができる。

syntax = "proto3";

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

それぞれのmessage typeには名前と型を持つユニークな数値のフィールドが含まれる。一度割り当てた数値のtypeは変更しないようにする。 (Assigning Tags)

repeatedは0以上の任意の個数で、順番は保存される。 (Specifying Field Rules)

新しいコードがフィールドを追加してメッセージを送ると、古いコードは未知のフィールドを無視する。 逆に、古いコードがメッセージを送ると、 新しいコードの新しいフィールドにはデフォルト値が入る。 (Updating A Message Type)

使い方

protoファイル。

syntax = "proto3";

message Data {
  string title = 1;
}

goのコードを生成。

$ brew install protobuf
$ protoc --version
libprotoc 3.1.0

$ go get github.com/golang/protobuf/protoc-gen-go

$ mkdir -p build/gen 
$ protoc --proto_path=proto --go_out=data proto/data.proto
$ ls data
data.pb.go

生成されたコード。

// Code generated by protoc-gen-go.
// source: data.proto
// DO NOT EDIT!

/*
Package data is a generated protocol buffer package.

It is generated from these files:
	data.proto

It has these top-level messages:
	Data
*/
package data

import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

type Data struct {
	Title string `protobuf:"bytes,1,opt,name=title" json:"title,omitempty"`
}

func (m *Data) Reset()                    { *m = Data{} }
func (m *Data) String() string            { return proto.CompactTextString(m) }
func (*Data) ProtoMessage()               {}
func (*Data) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }

func init() {
	proto.RegisterType((*Data)(nil), "Data")
}

func init() { proto.RegisterFile("data.proto", fileDescriptor0) }

var fileDescriptor0 = []byte{
	// 67 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x4a, 0x49, 0x2c, 0x49,
	0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x92, 0xe1, 0x62, 0x71, 0x49, 0x2c, 0x49, 0x14, 0x12,
	0xe1, 0x62, 0x2d, 0xc9, 0x2c, 0xc9, 0x49, 0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0x70,
	0x92, 0xd8, 0xc0, 0x8a, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf5, 0x0d, 0xbe, 0x9b, 0x32,
	0x00, 0x00, 0x00,
}
$ go get github.com/golang/protobuf/proto
func ProtoBufEncode(origin data.Data) []byte {
	encoded, err := proto.Marshal(&origin)
	if err != nil {
		panic(err)
	}

	return encoded
}

func ProtoBufDecode(bytes []byte) data.Data {
	var decoded data.Data
	if err := proto.Unmarshal(bytes, &decoded); err != nil {
		panic(err)
	}

	return decoded
}

FlatBuffers

Googleの、ゲームなどパフォーマンスを要求するアプリケーションのためのシリアライズライブラリ。 データにアクセスする前にparseやunpackする必要がなく、オブジェクトごとのメモリ割り当てが必要。

protoの代わりにこんな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; // 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 Weapon {
  name:string;
  damage:short;
}
root_type Monster;

使い方

schemaファイル。

namespace dataf;

table Data {
    title:string;
}

root_type Data;

goのコードを生成。

$ brew install flatbuffers

$ flatc --go fbs/dataf.fbs
$ ls dataf/
data.go

$ go get github.com/google/flatbuffers/go

生成されたコード。

// automatically generated by the FlatBuffers compiler, do not modify

package dataf

import (
	flatbuffers "github.com/google/flatbuffers/go"
)

type data struct {
	_tab flatbuffers.Table
}

func GetRootAsdata(buf []byte, offset flatbuffers.UOffsetT) *data {
	n := flatbuffers.GetUOffsetT(buf[offset:])
	x := &data{}
	x.Init(buf, n+offset)
	return x
}

func (rcv *data) Init(buf []byte, i flatbuffers.UOffsetT) {
	rcv._tab.Bytes = buf
	rcv._tab.Pos = i
}

func (rcv *data) Title() []byte {
	o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
	if o != 0 {
		return rcv._tab.ByteVector(o + rcv._tab.Pos)
	}
	return nil
}

func dataStart(builder *flatbuffers.Builder) {
	builder.StartObject(1)
}
func dataAddTitle(builder *flatbuffers.Builder, title flatbuffers.UOffsetT) {
	builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(title), 0)
}
func dataEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
	return builder.EndObject()
}

参考

Tutorial: Use FlatBuffers in Go · Robert Winslow