spring-boot で FlatBuffers を試してみた
FlatBuffersが気になってspring-bootを使って試してみたので忘れないうちに残します。
FlatBuffersはGoogleが作成したクロスプラットフォームのメッセージシリアライゼーションライブラリですが、同様なものにProtocolBuffersや、MessagePackなども有名だと思います。特に性能比較などは多くの記事でも照会されているので特に書かないですが、FlatBuffersは比較的新しいものでゲーム向けへの利用を考えて作成されたとあり、以下の特徴があるようです。また公式のサイトにbenchmarkの結果がありましたがとても性能が良さそうです。
- パース処理がなく高速にアクセスできる
- バッファメモリだけを利用することでデータアクセスを行うことができるため、省メモリに抑えられる
なんだか、モバイル環境のゲームなどには適していそうです。今回試した環境は以下です。 * Mac OS X 10.10 * JDK 1.8.0 u45
FlatBuffersを使えるようにする
flatbuffersをcloneして、flatcをビルドする
# git clone https://github.com/google/flatbuffers.git // XCode で開く # open flatbuffers/build/XCode/FlatBuffers.xcodeproj
flatc
バイナリがビルドされているので、PATHに追加する。今回は、/usr/local/binに追加した
# flatc -help 1 ↵ flatc: unknown commandline argument-help usage: flatc [OPTION]... FILE... [-- FILE...] -b Generate wire format binaries for any data definitions. -t Generate text output for any data definitions. -c Generate C++ headers for tables/structs. -g Generate Go files for tables/structs. -j Generate Java classes for tables/structs. -n Generate C# classes for tables/structs. -p Generate Python files for tables/structs. -o PATH Prefix PATH to all generated files. -I PATH Search for includes in the specified path. -M Print make rules for generated files. --strict-json Strict JSON: field names must be / will be quoted, no trailing commas in tables/vectors. --defaults-json Output fields whose value is the default when writing JSON --no-prefix Don't prefix enum values with the enum type in C++. --gen-includes (deprecated), this is the default behavior. If the original behavior is required (no include statements) use --no-includes. --no-includes Don't generate include statements for included schemas the generated file depends on (C++). --gen-mutable Generate accessors that can mutate buffers in-place. --gen-onefile Generate single output file for C# --raw-binary Allow binaries without file_indentifier to be read. This may crash flatc given a mismatched schema. --proto Input is a .proto, translate to .fbs. --schema Serialize schemas instead of JSON (use with -b) FILEs may depend on declarations in earlier files. FILEs after the -- must be binary flatbuffer format files. Output files are named using the base file name of the input, and written to the current directory or the path given by -o. example: flatc -c -b schema1.fbs schema2.fbs data.json
今回は、Javaで利用したいので、Java クライアントライブラリも準備する
# cd flatbuffers/java # mvn install
spring-boot で使ってみる
今回は、spring-bootで試したかったので以下のようなプロジェクトを作成した。
- pom.xml
... <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.google.flatbuffers</groupId> <artifactId>flatbuffers-java</artifactId> <version>1.2.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-afterburner</artifactId> <version>2.4.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.4.5</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.14.8</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.ning</groupId> <artifactId>async-http-client</artifactId> <version>1.9.31</version> </dependency> </dependencies> ...
以下のflatbuffersのIDLを簡単に作成してみた。
namespace org.horiga.study.springboot.flatbuffers.protocol.messages; table Token { id: int; accessToken: string; created: long; } table Me { token: Token; } table UserAnswer { displayName: string; mid: string; pictureUrl: string; }
IDLをflatcでコンパイルすると、IDLのnamespace
に定義した場所に-o
オプションで指定した場所からの位置にjavaのファイルが作成される。
# flatc -j -o src/main/java src/main/idl/fbs/v1.0.fbs
今回作成したFlatBuffersのためのspring-bootの処理は以下のようなクラスを追加してみた。
public class FlatBuffersHttpMessageConverter extends AbstractHttpMessageConverter<Table> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); public static final MediaType X_FLATBUFFERS = new MediaType("application", "x-fb", DEFAULT_CHARSET); public static final String X_FLATBUFFERS_MESSAGE_ID = "X-FBS-MessageId"; protected final Map<String, FlatBuffersMessage> messageRepository; public FlatBuffersHttpMessageConverter(Map<String, FlatBuffersMessage> messageRepository) { super(X_FLATBUFFERS); this.messageRepository = messageRepository; } @Override protected boolean supports(Class<?> clazz) { return Table.class.isAssignableFrom(clazz); } @Override protected Table readInternal(Class<? extends Table> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { final String messageId = inputMessage.getHeaders().getFirst(X_FLATBUFFERS_MESSAGE_ID); log.debug("Request.messageId: {}", messageId); if(Objects.isNull(messageId) || !messageRepository.containsKey(messageId)) { throw new HttpMessageNotReadableException("Unknown message protocol identifier"); } final long contentLength = inputMessage.getHeaders().getContentLength(); final ByteArrayOutputStream out = new ByteArrayOutputStream(contentLength >= 0 ? (int) contentLength : StreamUtils.BUFFER_SIZE); StreamUtils.copy(inputMessage.getBody(), out); return messageRepository.get(messageId).build(ByteBuffer.wrap(out.toByteArray())); } @Override protected void writeInternal(Table message, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { setFlatBuffersResponseHeaders(message, outputMessage); final byte[] dst = new byte[getContentLength(message, X_FLATBUFFERS).intValue()]; message.getByteBuffer().get(dst); StreamUtils.copy(dst, outputMessage.getBody()); } @Override protected Long getContentLength(Table table, MediaType contentType) throws IOException { final ByteBuffer buf = table.getByteBuffer(); return (long) (buf.limit() - buf.position()); } private void setFlatBuffersResponseHeaders(final Table message, final HttpOutputMessage outputMessage) { // debug } }
public class FlatBuffersMessage { private final String id; private final Class<?> klass; private final Method buildMethod; public FlatBuffersMessage(String id, Class<? extends Table> klass) throws Exception { this.id = id; this.klass = klass; final Method m = klass.getMethod("getRootAs" + klass.getSimpleName(), ByteBuffer.class); Preconditions.checkArgument(m != null, "This message is not FlatBuffers message"); this.buildMethod = m; } public Table build(final ByteBuffer bytes) throws FlatBuffersMessageProtocolException { try { return (Table) this.buildMethod.invoke(klass, bytes); } catch (Exception e) { throw new FlatBuffersMessageProtocolException("Unavailable flatbuffers message.", e); } } }
public class APIController { @RequestMapping(value = "/api", method = RequestMethod.POST) public Callable<Table> onMessage( @RequestBody Table message ) { // test if (message instanceof Me) { log.info("accessToken:{}", ((Me) message).token().accessToken()); log.info("id :{}", ((Me) message).token().id()); log.info("created :{}", ((Me) message).token().created()); } return () -> { FlatBufferBuilder fbb = new FlatBufferBuilder(0); fbb.finish(UserAnswer.createUserAnswer(fbb, fbb.createString("Hiroyuki Horigami"), fbb.createString("12345"), fbb.createString("//scontent-nrt1-1.xx.fbcdn.net/hphotos-xpa1/t31.0-8/891598_504258659637103_960802615_o.jpg"))); UserAnswer resultMessage = UserAnswer.getRootAsUserAnswer(UserAnswer.getRootAsUserAnswer(fbb.dataBuffer()).getByteBuffer()); return resultMessage; }; } }
簡単に説明すると、X-FB-MessageId
というリクエストのヘッダでなんのメッセージなのかを教えてもらって、それを元に受信したメッセージを読み取る。そして、MediaTypeはapplication/x-fb
にしておいた。
もう少し改良するとすれば、X-FB-MessageId
にメッセージのIDを定義するのではなく、FlatBuffersの受信・応答メッセージの先頭部分にメッセージのIDを表す識別子(例えばshort
typeの数字など)を定義したりJWTのように何か改ざんできないような仕組みを考えたかったけど、今回は簡単に実装しました。
ひとまず、ベンチマークとかとってないけど、簡単に使えそうだし良さそうな印象です。
さいごに
まだ詳しく調べてないので間違った部分もあると思います。指摘があればよろしくお願いいたします。