Unity & サーバー間通信でのMessagePack導入奮闘記(サーバー導入編)

こんにちは。新卒でサーバーサイドエンジニアをしている向井です。

今回からは、MessagePackをプロジェクトでの導入するにあたっての実用話や苦労話を、複数回に分けてお届けします。

本エントリでは、新規プロジェクトでMessagePackの導入に至った背景と、サーバーサイドでのMessagePackの導入について紹介します。

導入の背景

まず、MessagePackを導入の背景について説明します。

今回、新規のUnityプロジェクトでの開発ということもあり、弊社でスタンダードな基盤が確立されていませんでした。基盤の実装にあたり、サーバー・クライアント間での通信のフォーマットも最適なものを選定したいと考えていました。

異なるプログラミング言語間や、端末・Webサーバ間でデータを送受信する際には、その間で決まったフォーマットでデータをやり取りする必要があります。XMLやJSON・MessagePackなどがそのフォーマットになります。

最もポピュラーなフォーマットはご存知の通りJSONかと思います。弊社でも多くのプロジェクトでJSONが採用されています。

各フォーマットの詳しい説明は省略し、簡単に判断基準になった点を挙げます。

  • JSON: 幅広く普及している。ただし、Unity且つソーシャルゲーム等、レスポンスが肥大化する場面では、特にデシリアライズ(リフレクション)がボトルネックとなり、処理が遅くなることがある。
  • MessagePack:バイナリ形式なので、JSONと比べてデータのサイズが小さく、またシリアライズ・デシリアライズのが高速である。バイナリ形式のためデータを人が読めないので、JSONと比べてデバッグが難しい。
  • Protocol Buffers: MessagePackと同じくバイナリ形式である。MessagePackの方が処理が高速という情報が多い。また事前にインタフェース定義言語(IDL)による構造の定義が必要である。

速度面を考慮するとMessagePackとProtocol Buffersの2点が候補となり、同じバイナリ形式の中では、扱いやすいであろうMessagePackを採用することになりました。

Array型とMap型のMessagePack

MessagePack形式でクラスや構造体をシリアライズ・デシリアライズする方法は、大きく分けてArray型とMap型があります。

それぞれの要点をまとめると以下とおりです。

  • Map型
    • JSONと同様にクラスのプロパティ名とプロパティ値をkey-valueのペアとしたマップをMessagePackのmap format familyによってシリアライズ・デシリアライズする方法。
    • メリット: クラスの情報を事前にMessagePackでやり取りするクライアント間で共有することがないため、JSONのように便利に扱うことが出来る。
    • デメリット: クラスのプロパティ名を文字列として送るために、大量のリストデータを送る時には冗長なデータとなる。
  • Array型
    • クラスのプロパティに順番をつけてき、その順番でプロパティ値を配列し、MessagePackのarray format familyによってシリアライズ・デシリアライズする方法。
    • メリット: プロパティ名を送らない分データは小さくなる。
    • デメリット: 事前にMessagePackでやり取りするサーバーとクライアント間でクラス情報を共有する必要が有るため、Map型と比べると保守性が下がる。

今回のプロジェクトでは、通信が不安定なモバイルのネットワーク下でもマスタデータなどの大量のリストデータをやり取りするパフォーマンスを向上させるために、後者のArray型を採用しました。

この方法では、クライアント・サーバー間でクラス情報の齟齬が発生すると通信できない問題があるために、スプレッドシートで生成したAPIドキュメントから、クライアントとサーバーのクラスファイルを自動生成するツールを作成することで開発の効率化を図っています。この方法についてはまたの機会にご紹介したいと思います。

サーバーサイド(Java)でのMessagePackの導入

ここからは、実際にサーバーサイドで上記のMessagePackを導入する方法について述べます。

私の担当しているプロジェクトでは、サーバーサイドはJava + Spring4 MVC が採用されているので、Java + Spring MVC でのMessagePackの導入について説明します。プロジェクトで導入している各ライブラリのバージョンは以下のとおりです。

  • Java 1.8
  • Spring 4.1.4
  • msgpack-java 0.6.12

MessagePackのライブラリの選定

JavaでMessagePackを扱うライブラリには、公式が提供しているmsgpack-java を用いました。

msgpack-javaにはv6とv7が存在しますが、導入を決定した当時には絶賛開発中であったため、今回はMavenRepositoryの最新版として登録されているv6を採用しました。

このv6では、オブジェクトのシリアライズ(JavaオブジェクトをMessagePackバイナリへの変換)が先述したArray形式であるため、ライブラリをほぼそのまま利用しています。

例として、オブジェクトのシリアライズとデシリアライズのサンプルコードを以下に示します。

MessagePack messagePack = new MessagePack();

ByteArrayOutputStream out = new ByteArrayOutputStream();
Packer packer = messagePack.createPacker(out);

User user = new User();
user.id = 1L;
user.name = "sampleuser";
user.password = "sampleusersecretpassword";

packer.write(user);
byte[] bytes = out.toByteArray();

ByteArrayInputStream in = new ByteArrayInputStream(bytes);
Unpacker unpacker = messagePack.createUnpacker(in);

User user_ = unpacker.read(User.class);

System.out.println(user_.id); // 1
System.out.println(user_.name); // sampleuser
System.out.println(user_.password); // null

Userクラスの定義は以下のとおりです。

@Message
public class User {
    @Index(0)
    public Long id;

    @Index(1)
    public String name;

    @Ignore
    public String password;
}

msgpack-javaは、MessageアノテーションがついているクラスであればMessagePackへの変換ができます。また、アノテーションベースでMessagePackの変換の挙動を制御することができます。例えば、変換したいクラスのメンバ変数(上記の例だとUserクラスのid, name変数)にIndexアノテーションをつけることによりMessagePackのシリアライズ順を指定できます。このアノテーションにより、先述したArray型で必要なプロパティ順を明記することで、プロパティのシリアライズ順を保証します。
また、Ignoreアノテーションをつけたメンバ変数はMessagePackのシリアライズ・デシリアライズの対象から除外するなどの指定ができます。上記のUserクラスでは、password変数にIgnoreが指定されているため、シリアライズしてMessagePackに変換したバイナリをデシリアライズするとpasswordがnullになっていることが確認できます。

Spring MVC で MessagePack を扱う

Spring MVC では、RequestBodyアノテーションをコントローラのメソッド内の引数につけることで、HTTPリクエスト本文を引数の型に合わせて自動でマッピングします。

また、ResponseBodyアノテーションをメソッドにつけることで、戻り値の型の情報を元にHTTPレスポンス本文を自動で生成します。

このような機能は、HttpMessageConvertersという仕組みにより実現されています。

HttpMessageConvertersは、HTTPリクエスト(特にContent-TypeとAccept)から上記の変換を行うクラスを選択し、そのクラスに処理を移譲します。

SpringMVCの4系では、JSONなどの主要なフォーマットの処理クラスが標準で用意されています。また、標準で用意されていないフォーマットは、独自のAbstractHttpMessageConverterを継承した処理クラスを実装することで対応させることが出来ます。

Array型のMessagePackは標準で用意されていないので、その実装について説明していきます。

import org.msgpack.MessagePack;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

public class MessagePackHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    /** コンテントタイプ */
    public static final String DEFAULT_TYPE = "application";

    /** コンテントタイプのサブタイプ */
    public static final String DEFAULT_SUBTYPE = "x-msgpack";

    /** デフォルトエンコード */
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    /**
     * コンストラクタでMediaTypeのフィルタとmessagePackオブジェクトの初期化を行う
     */
    public MessagePackHttpMessageConverter() {
        // こので指定したメディアタイプにのみ、このクラスが適用される。
        super(new MediaType(DEFAULT_TYPE, DEFAULT_SUBTYPE, DEFAULT_CHARSET));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }
    
    // クライアントからのリクエスト受信
    @Override
    protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        MessagePack msgpack = new MessagePack();
        // inputMessage.getBody()にリクエストボディが格納されている
        // clazzにはコントローラのアクションが受け取る型情報が渡される
        return msgpack.read(inputMessage.getBody(), clazz);
    }

    // クライアントへのレスポンス送信
    @Override
    protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        MessagePack msgpack = new MessagePack();
        // outputMessageにMessagePackバイナリを書き込む
        outputMessage.getBody().write(msgpack.write(obj));
    }
}

先述の通り、AbstractHttpMessageConverterを継承したクラスを実装します。まず、コンストラクタで、親のコンストラクタをMediaTypeのインスタンスを引数に呼び出します。これでMessagePackHttpMessageConverterは、application/x-msgpackがメディアタイプとして指定された場合に呼び出されるようになります。

次に、リクエストボディをオブジェクトにマッピングする処理と、オブジェクトをレスポンスボディに変換する処理をそれぞれreadInternalとwriteInternalで実装しています。先ほど説明した方法で、MessagePackのシリアライズ・デシリアライズを行っています。

これで、HttpMessageConverterの実装は完了です。あとは、設定ファイルからMessagePackHttpMessageConverterを読み込ませます。

<mvc:annotation-driven>
    <mvc:message-converters>
        <!--
        @RequestBodyが指定されている時、Content-Type:application/x-msgpackとした場合、リクエストをMessagePackとして扱います。
        @ResponseBodyが指定されている時、Accept:application/x-msgpackとした場合、レスポンスをMessagePackで返します。
         -->
        <bean class="com.example.MessagePackHttpMessageConverter">
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

HttpMessageConvertersに、Array型のMessagePackフォーマットの処理を追加する事ができました。

コントローラでは先述の通り、@RequestBodyをメソッドの引数につけることでContent-Typeがapplication/x-msgpackが指定されている時に、リクエストをArray型のMessagePackとみなしてSampleRequestクラスにマッピングします。また@ResponseBodyをメソッドの戻り値につけることで、Acceptにapplication/x-msgpackが指定されている場合に、SampleResponseをArray型のMessagePackとして返却します。

public class SampleController {

    @RequestMapping("/sample")
    @ResponseBody
    public SampleResponse sample(@RequestBody SampleRequest request) {

        SampleResponse response = new SampleResponse();
        response.id = request.id;
        response.name = request.name;

        return response;
    }
}

// リクエストクラス
@Message
public class SampleRequest {
    @Index(0)
    public Long id;
    @Index(1)
    public String name;
}

// レスポンスクラス
@Message
public class SampleResponse {
    @Index(0)
    public Long id;
    @Index(1)
    public String name;
}

余談ですが、HttpMessageConverterContent-TypeAcceptに応じて処理クラスを振り分けることが出来るため、application/jsonの時はJSONを解釈するようにしておくと、APIのデバッグなどになにかと便利です。

まとめ

MessagePack導入の背景からJavaでMessagePackを扱う方法、Spring MVC のプロジェクトにMessagePackを導入する方法の一つとして、HttpMessageConverterのMessagePack実装について紹介しました。

次回は、クライアント(Unity)でのMessagePackの導入方法と、パフォーマンスについて説明したいと思います。

参考資料