WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

軽量高速な「MessagePack」でデータをシリアライズしてみる

リニューアル後、3エントリー目、担当の長屋です。 日々、業務中にいいなと思ったことを書いてみます。

今回は汎用的で且つ軽量高速なバイナリベースのシリアライズ形式である「MessagePack」について書いてみようと思います。JSON-likeに汎用的に扱え、バイナリ形式で且つ余分なデータを切り詰めているためシリアライズ後のサイズはJSONよりもかなり小さくなります。

また、C++の場合は自作のデーダオブジェクトや列挙型などのそのままシリアライズ・デシリアライズできる楽ちん仕様になってます。

MessagePack
MessagePack: It's like JSON. but fast and small.

実際にMessagePack形式でシリアライズしたデータのサイズとJSONの場合のサイズを比べてみたいと思います。

#include <iostream>
#include <msgpack.hpp>

using namespace std;

void test01()
{
    //シリアライズ用テストデータ
    map<string,int> tests = {{"AAAA",100},{"BBBB",121},{"CCCC",50},{"DDDD",25}};
    
    //テストデータと同じ構造のJSON文字列
    const char * jsonStr = "{\"AAAA\":100,\"BBBB\":121,\"CCCC\":50,\"DDDD\":25}";
    
    //シリアライズ用のバッファ
    msgpack::sbuffer buff;
    
    //シリアライズ
    msgpack::pack(&buff, tests);
    
    //シリアライズされたデータ。
    //調整されたバイナリデータになっておりかなり軽量
    char * data = buff.data();
    
    //msgpackとjsonでサイズを比べてみる
    cout << "MsgPack size" << strlen(data) << endl;
    cout << "json size" << strlen(jsonStr) << endl;
    
    //シリアライズされたデータをデシリアライズしてみる
    msgpack::object_handle oh = msgpack::unpack(data, buff.size());
    msgpack::object obj = oh.get();
    
    map<string,int> deserializeDatas;
    obj.convert(deserializeDatas);
    
    //デシリアライズしたデータの出力
    for (auto data : deserializeDatas)
        cout << data.first << ":" << data.second << endl;
}

出力結果

MsgPack size25
json size43
AAAA:100
BBBB:121
CCCC:50
DDDD:25

長さベースのサイズ比較ですが、JSONに比べて約60%近くにまでサイズがカットされています。

次は自作のデータオブジェクトをそのままシリアライズ・デシリアライズしてみます。 C++の場合自作のデータオブジェクトや列挙型でも提供されているマクロを用いることで簡単にシリアライズとデシリアライズができるようになっています。

#include <iostream>
#include <msgpack.hpp>

using namespace std;

//テスト列挙型
enum class TestType : int
{
    Unknown,
    A = 100,
    B = 200,
    C = 300
};

//シリアライズしたい列挙型を設定
MSGPACK_ADD_ENUM(TestType);

//テストオブジェクトその1
struct TestData
{
    string name;
    int id;
    float value;
    TestType type;
    
    //シリアライズしたい変数を設定
    MSGPACK_DEFINE(name,id,value,type);
};

//テストオブジェクトその2
struct TestDataGroup
{
    int groupId;
    vector<TestData> testDatas;
    
    //シリアライズしたい変数を設定
    MSGPACK_DEFINE(groupId,testDatas);
};

void test02()
{
    //テストデータグループAを作成
    TestDataGroup groupA;
    groupA.groupId = 10;
    groupA.testDatas.push_back({"AAAA",1,123.456f,TestType::A});
    groupA.testDatas.push_back({"BBBB",2,180.554f,TestType::B});
    groupA.testDatas.push_back({"CrashFever!",3,10.0f,TestType::C});
    
    //テストデータグループBを作成
    TestDataGroup groupB;
    groupB.groupId = 20;
    groupB.testDatas.push_back({"CCCC",1,100.0f,TestType::A});
    groupB.testDatas.push_back({"DDDD",2,110.0f,TestType::B});
    groupB.testDatas.push_back({"EEEE",3,120.0f,TestType::C});
    
    //シリアライズ用のオブジェクトを作成
    map<string,TestDataGroup> testDataGroups;
    testDataGroups["GroupA"] = groupA;
    testDataGroups["GroupB"] = groupB;
    
    //シリアライズ
    msgpack::sbuffer sbuf;
    msgpack::pack(sbuf, testDataGroups);
    
    //デシリアライズ
    msgpack::object_handle oh = msgpack::unpack(sbuf.data(), sbuf.size());
    msgpack::object data = oh.get();
    map<string,TestDataGroup> deserializeDatas;
    data.convert(deserializeDatas);
    
    //デシリアライズした物を出力してみる
    for (const auto & group : deserializeDatas)
    {
        cout << "key = " << group.first << " GroupID = "<< group.second.groupId << endl;
        for (const auto & data : group.second.testDatas)
        {
            cout << data.name << " " << data.id << " " << data.value << " " << (int)data.type << endl;
        }
        cout << endl;
    }
}

出力結果

key = GroupA GroupID = 10
AAAA 1 123.456 100
BBBB 2 180.554 200
CrashFever! 3 10 300

key = GroupB GroupID = 20
CCCC 1 100 100
DDDD 2 110 200
EEEE 3 120 300

このように自作のオブジェクトであっても簡単にシリアライズ・デシリアライズができます。また各オブジェクト用にシリアライザなどを作る必要がないのも大きな利点です。
ただ、シリアライズされたデータはバイナリ形式になるため人の目で見ることができないので使い分けが必要な部分は出てくるとおもいます。
弊社の「CrashFever」の場合はサーバーとのやり取りはJSONで行いマルチプレイ時の速度が必要な場面はMessagePackを使用しています。