ProtocolBuffersの後継ライブラリ、FlatBuffersをJavaで使ってみる。
前提
- 2014/9/17時点のFlatBuffers。
- 開発環境はMac。
- サンプルコードの中でguava-librariesのListsとIntsを使用。
FlatBuffersとは
FlatBuffersはGoogleが2014/6から公開しているシリアライズライブラリ。
ゲーム開発向けに作られていて、
- データアクセスに必要なメモリはバッファのメモリのみで省メモリ
- パース不要なのでオーバーヘッドが少なく高速
- 定義ファイルから生成するため型安全
- クロスプラットフォーム
などが特徴。
FlatBuffers自体まだ開発途中だが、特にJava対応はまだまだ未完成。
enumで定義したものが普通の定数で作成されたり、Json形式が未対応だったりする。
詳しくはこちら。
準備
FlatBuffersはまだパッケージが用意されていないので、自分でコンパイルする必要がある。
MacはXcodeを使用する。WindowsはVisualStudioでコンパイルできるはず。
- 任意のディレクトリにGitHubからclone。
git clone https://github.com/google/flatbuffers.git
- Xcodeでプロジェクトを開く。
open flatbuffers/build/Xcode/FlatBuffers.xcodeproj
- メニューのProduct-Scheme-flatcを選択。
- メニューのProduct-Buildをクリックしてコンパイル。
- flatbuffers/flatcが作られるので、任意のパスが通ってる所にリンクを作る。
ln -s flatbuffers/flatc /usr/local/bin/flatc
chmod 755 /usr/local/bin/flatc
クラスファイル生成
まず定義ファイルを作る。
よくあるユーザ情報と、そのリストを定義。
namespace entity;
table FUser {
name: string;
lv: short;
lastLogin: long;
message: string;
}
table FUsers {
user: [FUser];
}
この定義ファイルを使ってflatcを実行する。
flatc -j -o src/main/java user.fbs
実行するとsrc/main/java/entity以下にFUser.javaとFUsers.javaが生成される。
生成したクラスはFlatBuffersのクラスに依存しているが、現在jarが用意されていないので、FlatBuffersのプロジェクトからコピーしてくる。
ファイル構成は以下のようになる。
|
---- user.fbs (定義ファイル)
|
---- src/main/java
|
---- User.java (サンプルコードで使用するDTO)
|
---- entity (flatcで生成したクラスファイル)
| |
| ---- FUser.java
| |
| ---- FUsers.java
|
---- com.google.flatbuffers (FlatBuffersからコピーしてきたクラスファイル)
|
---- Constants.java
|
---- FlatBufferBuilder.java
|
---- Struct.java
|
---- Table.java
シリアライズ
シリアライズはFlatBufferBuilderとflatcで生成したクラスを使用して行う。
FUser
User user = new User("hoge", (short)1, System.currentTimeMillis(), "hello");
FlatBufferBuilder fbb = new FlatBufferBuilder(0);
// StringはあらかじめFlatBufferBuilderに入れてoffsetを取得しておく
int nameOffset = fbb.createString(user.name());
int messageOffset = fbb.createString(user.message());
// FUser開始
FUser.startFUser(fbb);
// 文字列はoffset、その他は値をセット
FUser.addName(fbb, nameOffset);
FUser.addLv(fbb, user.lv());
FUser.addLastLogin(fbb, user.lastLogin());
FUser.addMessage(fbb, messageOffset);
// FUser終了
int rootTable = FUser.endFUser(fbb);
// rootのoffsetを渡してFlatBufferBuilderを終了
fbb.finish(rootTable);
// 開始位置を0に修正したバイナリを取得
byte[] binary = fbb.sizedByteArray();
パッキングせずにバイナリを組み立てているだけなので省メモリかつ高速。
バイナリ取得はdataBufferではなくsizedByteArrayを使用する。
前者だと、デシリアライズ時にByteBufferのpositionの値が必要になってくるので使いにくい。
FUsers
List<User> users = Lists.newArrayList();
users.add(new User("hoge", (short)1, System.currentTimeMillis(), "hello"));
users.add(new User("fuga", (short)5, System.currentTimeMillis(), "hi"));
FlatBufferBuilder fbb = new FlatBufferBuilder(0);
// 各FUserのoffsetを保持するリストを用意
List<Integer> userOffsets = Lists.newArrayList();
for (User user : users) {
// StringはあらかじめFlatBufferBuilderに入れてoffsetを取得しておく
int nameOffset = fbb.createString(user.name());
int messageOffset = fbb.createString(user.message());
// FUser開始
FUser.startFUser(fbb);
// 文字列はoffset、その他は値をセット
FUser.addName(fbb, nameOffset);
FUser.addLv(fbb, user.lv());
FUser.addLastLogin(fbb, user.lastLogin());
FUser.addMessage(fbb, messageOffset);
// FUser終了、rootのoffsetをリストに追加
userOffsets.add(FUser.endFUser(fbb));
}
// offsetのリストを反転する
userOffsets = Lists.reverse(userOffsets);
// UserVector開始
FUsers.startUserVector(fbb, userOffsets.size());
for (int userOffset : userOffsets) {
// FUserのoffsetをセット
fbb.addOffset(userOffset);
}
// UserVector終了
int usersOffset = fbb.endVector();
// FUsers開始
FUsers.startFUsers(fbb);
// UserVectorのoffsetをセット
FUsers.addUser(fbb, usersOffset);
// FUsers終了
int rootTable = FUsers.endFUsers(fbb);
// rootのoffsetを渡してFlatBufferBuilderを終了
fbb.finish(rootTable);
// 開始位置を0に修正したバイナリを取得
byte[] binary = fbb.sizedByteArray();
リストのシリアライズはかなり複雑になる。
まず、FlatBuffersBuilderはstartとendは1つずつ順に行う必要がある。
何かをstartしている間に別のものをstartしてしまうと、生成されるバイナリがおかしくなってしまうようだ。
startFUsersをしてからstartUserVectorし、startFUserをして構築したバイナリは、デシリアライズできなかった。
また、FUserのoffsetを入れたリストを反転することも重要。
反転しないと、デシリアライズ時にリストの順番が逆になってしまう。
※startUserVectorではなくcreateUserVectorを使用すると自分で反転しなくて済む。
int usersOffset = FUsers.createUserVector(fbb, Ints.toArray(userOffsets));
デシリアライズ
シリアライズは複雑だが、デシリアライズは簡単。
flatcで生成したクラスだけ使用する。
FUser
ByteBuffer bb = ByteBuffer.wrap(binary);
FUser user = FUser.getRootAsFUser(bb);
String name = user.name();
short lv = user.lv();
long lastLogin = user.lastLogin();
String message = user.message();
シリアライズしたバイナリをByteBufferに入れ、FUserに渡すだけでOK。
アンパッキングしていないので省メモリで高速。
シリアライズ時にdataBufferを使用してバイナリを取得した場合、ByteBufferのpositionにシリアライズ時の値をセットする必要がある。
FUsers
ByteBuffer bb = ByteBuffer.wrap(binary);
FUsers f = FUsers.getRootAsFUser(bb);
int length = f.userLength();
for (int i = 0; i < length; i++) {
FUser user = f.user(i);
String name = user.name();
short lv = user.lv();
long lastLogin = user.lastLogin();
String message = user.message();
}
リストのデシリアライズも、シリアライズしたバイナリをByteBufferに入れ、FUsersに渡すだけでOK。
シリアライズ時にFUserのoffsetを入れたリストを反転していないと、最後に入れたFUserから取得することになる。
また、Iteratorの取得はできないので、拡張for文は使えない。
感想
- 現状ではjarがなかったり機能が少なすぎたり、色々と不足している。
- シリアライズが特殊で複雑。慣れるまで大変。
- DTOでラップしたり、変換Utilクラスを作れば利用しやすい。
- 省メモリで高速なのは魅力的。JacksonやMessagePackより速い。