このエントリーではobjectとJSONとXMLの変換をやってみましたが、以前から気になっていたMessagePackを試してみました。
JSONとXMLはいずれもフォーマット定義(要はタグ)と値をstringとして最終的に扱いますが、MessagePackはフォーマット定義と値をバイナリとして扱います。そのためXMLのような構造のデータは非常に軽量にできます。まあMessagePackに限らず他にもバイナリ型のフォーマットは存在するけど、アルゴリズムが違ったり使いやすさもいろいろとあることでしょう。
どれがどうでこうでというのはneueさんのサイトとかとても参考になります。
MemcachedTranscoder – C#のMemcached用シリアライザライブラリ
.NETの標準シリアライザ(XML/JSON)の使い分けまとめ
実際にやってみる。
難しいことはさておき、早速やってみます。
例としてスーパーマーケットの店舗別の店員、商品情報等をデータクラス化して、それぞれJSON、XML、MessagePackに変換して保存してみます。
データクラス
public class Format
{
public enum 雇用形態種別
{
社員,
パート
}
public class 店員
{
public 雇用形態種別 雇用形態 { get; set; }
public string 名前 { get; set; }
public int 年齢 { get; set; }
public int 給与 { get; set; }
public 店員()
{
雇用形態 = 雇用形態種別.社員;
名前 = "";
年齢 = 給与 = 0;
}
public void Disp()
{
var str = string.Format("名前[{0}],雇用形態[{1}],年齢[{2}],給与[{3}]",
名前, 雇用形態.ToString(), 年齢, 給与);
Console.WriteLine(str);
}
}
public class 商品
{
public string 商品名 { get; set; }
public int 売価 { get; set; }
public int 原価 { get; set; }
public int 在庫数 { get; set; }
public int 販売数 { get; set; }
public int 売上()
{
return 売価 * 販売数;
}
public 商品()
{
商品名 = "";
売価 = 原価 = 在庫数 = 販売数 = 0;
}
public void Disp()
{
var str = string.Format("商品名[{0}],売価[{1}],原価[{2}],在庫数[{3}],販売数[{4}],売上[{5}]",
商品名, 売価, 原価, 在庫数, 販売数, 売上());
Console.WriteLine(str);
}
}
public class 店舗
{
public string 店舗名 { get; set; }
public 店員 店長 { get; set; }
public List<店員> メンバー { get; set; }
public List<商品> 店舗商品 { get; set; }
public 店舗()
{
店舗名 = "";
店長 = new 店員();
メンバー = new List<店員>();
店舗商品 = new List<商品>();
}
public void Disp()
{
var str = string.Format("\n----------------");
str += string.Format("店舗名[{0}],店長[{1}]", 店舗名, 店長.名前);
Console.WriteLine(str);
Console.WriteLine("店員情報");
メンバー.ForEach(p => p.Disp());
Console.WriteLine("商品情報");
店舗商品.ForEach(p => p.Disp());
Console.WriteLine("\n");
}
}
public class 店舗管理
{
public List<店舗> 店舗情報{get;set;}
public 店舗管理()
{
店舗情報 = new List<店舗>();
}
public void Disp()
{
店舗情報.ForEach(p => p.Disp());
}
}
public class 管理
{
public 店舗管理 管理情報{get;set;}
public 管理()
{
管理情報 = new 店舗管理();
}
public void Disp()
{
管理情報.Disp();
}
}
}
MessagePackで対象となるクラスを定義する際の注意点(たぶん)
・データをPackする対象のクラス・プロパティはすべてPublicにする。
・コンストラクタを定義する際は下記のデフォルトのコンストラクタを記述する。
public class DataClass
{
public string Str{ get; set;}
/// 他のコンストラクタを定義した場合は、
/// デフォルトコンストラクタを必ず定義する
public DataClass()
{
Str = "";
}
/// 自分で定義したコンストラクタ
public DataClass(string str)
{
Str = str;
}
}
ダミーのデータを作成する
管理クラスに対してダミーのデータをセットします。
static void SetData(out Format.管理 manage)
{
var Names = new[] { "山田", "森", "大島", "野中", "中山", "小島", "中田", "吉田", "柳瀬", "井上", "小森", "佐藤", "小林", "松井", "篠田", "佐田" };
var rnd = new Random(Environment.TickCount);
Func<int,List<Format.店員>> GetMembers = (num) =>
{
var ret = new List<Format.店員>();
Enumerable.Range(0,num).ToList().ForEach(p=>
{
var member = new Format.店員();
member.名前 = Names[ rnd.Next(0, Names.Count() - 1)];
member.雇用形態 = rnd.Next(0,100000) % 2 == 0 ? Format.雇用形態種別.社員 : Format.雇用形態種別.パート;
member.年齢 = rnd.Next(18, 60);
member.給与 = member.雇用形態 == Format.雇用形態種別.社員 ? (rnd.Next(20, 30) * 10000) : (rnd.Next(15, 20) * 10000);
ret.Add(member);
});
return ret;
};
Func<List<Format.商品>> GetGoods = () =>
{
var goods = new List<Format.商品>();
goods.Add(new Format.商品() { 商品名 = "酒A", 売価 = 1000, 原価 = 800 , 在庫数 = rnd.Next(0,30), 販売数 = rnd.Next(0,30)});
goods.Add(new Format.商品() { 商品名 = "酒B", 売価 = 2000, 原価 = 1500, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "酒C", 売価 = 2400, 原価 = 1650, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "菓子A", 売価 = 100, 原価 = 60, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "菓子B", 売価 = 50, 原価 = 10, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "文具A", 売価 = 100, 原価 = 80, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "鮮魚A", 売価 = 400, 原価 = 150, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "鮮魚B", 売価 = 980, 原価 = 700, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "肉A", 売価 = 398, 原価 = 240, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "肉B", 売価 = 1000, 原価 = 800, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "野菜A", 売価 = 298, 原価 = 150, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
goods.Add(new Format.商品() { 商品名 = "野菜B", 売価 = 398, 原価 = 300, 在庫数 = rnd.Next(0, 30), 販売数 = rnd.Next(0, 30) });
return goods;
};
var StoreNames = new[] { "本店", "駅前店", "1丁目店", "4丁目店", "モール店", "中央店", "北店", "東店", "南店", "西店", "市場店" };
Func<string, Format.店舗> GetStoreInfo = (name) =>
{
var ret = new Format.店舗() { 店舗名 = name };
ret.メンバー = GetMembers(rnd.Next(10, 20));
ret.店長 = ret.メンバー.Where(p => p.雇用形態 == Format.雇用形態種別.社員).First();
ret.店舗商品 = GetGoods();
return ret;
};
var StoresInfo = new Format.店舗管理();
StoreNames.ToList().ForEach(p => StoresInfo.店舗情報.Add(GetStoreInfo(p)));
manage = new Format.管理() { 管理情報 = StoresInfo };
}
ダミーのデータは次のようなデータが出来上がる
1店舗あたりのデータは次のデータが出来上がります。色々突っ込みどころはありますが、とりあえずテストなので。このデータが11店舗分できあがります。
----------------店舗名[市場店],店長[柳瀬]
店員情報
名前[柳瀬],雇用形態[社員],年齢[33],給与[230000]
名前[小島],雇用形態[パート],年齢[33],給与[160000]
名前[小林],雇用形態[パート],年齢[42],給与[190000]
名前[小森],雇用形態[社員],年齢[50],給与[230000]
名前[松井],雇用形態[パート],年齢[21],給与[170000]
名前[大島],雇用形態[社員],年齢[59],給与[250000]
名前[小島],雇用形態[パート],年齢[54],給与[160000]
名前[篠田],雇用形態[社員],年齢[40],給与[270000]
名前[大島],雇用形態[社員],年齢[39],給与[250000]
名前[柳瀬],雇用形態[パート],年齢[48],給与[150000]
名前[佐藤],雇用形態[パート],年齢[34],給与[150000]
名前[井上],雇用形態[社員],年齢[28],給与[200000]
名前[吉田],雇用形態[社員],年齢[46],給与[260000]
名前[小林],雇用形態[社員],年齢[47],給与[220000]
名前[野中],雇用形態[パート],年齢[58],給与[170000]
名前[中田],雇用形態[パート],年齢[33],給与[150000]
名前[大島],雇用形態[パート],年齢[51],給与[170000]
名前[吉田],雇用形態[社員],年齢[49],給与[260000]
商品情報
商品名[酒A],売価[1000],原価[800],在庫数[10],販売数[26],売上[26000]
商品名[酒B],売価[2000],原価[1500],在庫数[24],販売数[17],売上[34000]
商品名[酒C],売価[2400],原価[1650],在庫数[29],販売数[25],売上[60000]
商品名[菓子A],売価[100],原価[60],在庫数[28],販売数[3],売上[300]
商品名[菓子B],売価[50],原価[10],在庫数[17],販売数[23],売上[1150]
商品名[文具A],売価[100],原価[80],在庫数[23],販売数[29],売上[2900]
商品名[鮮魚A],売価[400],原価[150],在庫数[1],販売数[25],売上[10000]
商品名[鮮魚B],売価[980],原価[700],在庫数[29],販売数[7],売上[6860]
商品名[肉A],売価[398],原価[240],在庫数[15],販売数[8],売上[3184]
商品名[肉B],売価[1000],原価[800],在庫数[8],販売数[23],売上[23000]
商品名[野菜A],売価[298],原価[150],在庫数[7],販売数[6],売上[1788]
商品名[野菜B],売価[398],原価[300],在庫数[23],販売数[20],売上[7960]
objectからMessagePack(byte[])に変換する拡張メソッドを作成する
MessagePackは最終的にバイト配列が出来上がります。JSONやXMLは割りとライブラリ任せな部分があったけど、MessagePackはstreamを作ってあげます。
objectクラスにobjectからMessagePackへのPack,Unpackを行う拡張メソッドを作りたいと思います。
/// <summary>
/// object -> MessagePack(byte[])
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static byte[] Object2MessagePack<T>(this object obj)
{
var serializer = MessagePackSerializer.Create<T>();
var ms = new System.IO.MemoryStream();
serializer.Pack(ms, (T)obj);
var ret = ms.ToArray();
ms.Close();
return ret;
}
/// <summary>
/// MessagePack(byte[]) -> object
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="bytes"></param>
/// <returns></returns>
public static T MessagePack2Object<T>(this byte[] bytes)
{
var serialize = MessagePackSerializer.Create<T>();
var ms = new System.IO.MemoryStream(bytes);
var ret = serialize.Unpack(ms);
ms.Close();
return ret;
}
JSON <-> MessagePackやXML <-> MessagePackなどはメソッド同士を組み合わせることで可能ですが、変換処理のオーバーヘッドが大きいので必要なければ作成しなくてもいいと思います。
JSON,XML,MessagePackに変換する
次のソースではテストデータをJSON,XML,MessagePack(byte[])で出力し、MessagePackのデータに関しては出力したデータを読み取って、元データと読み取りデータの文字列(JSON)を比較し、値が同じであるか確認しています。
static void Main(string[] args)
{
var manage = new Format.管理();
/// テストデータ作成
SetData(out manage);
/// テストデータ表示
manage.Disp();
/// JSON形式で出力
System.IO.File.WriteAllText("manage.json", manage.Object2Json());
/// XML形式で出力
manage.Object2XDocument().Save("manage.xml");
/// MessagePack形式で出力
System.IO.File.WriteAllBytes("manage.bin", manage.Object2MessagePack<Format.管理>());
/// MessagePack形式で読み込み
var readData = System.IO.File.ReadAllBytes("manage.bin").MessagePack2Object<Format.管理>();
/// 読み取りデータ表示
readData.Disp();
/// 元データと読み取りデータのJSON(文字列)を取得
var str1 = manage.Object2Json();
var str2 = readData.Object2Json();
/// 比較してみる
if (str1 == str2)
{
Trace.WriteLine("Equals");
}
else
{
Trace.WriteLine("Not Equals");
}
Console.ReadKey();
}
実行結果
Equals
ファイル出力結果
驚嘆すべきはファイルのサイズです。※テストデータはランダムに作成されるので毎回同じサイズが出来るわけではない。

manage.bin 5,934 バイト
manage.json 22,314 バイト
manage.xml 56,078 バイト
XML対比10分の1の大きさにまでダイエットできました。全てをバイナリに置き換えているので小さくすむという単純な理由ですが、同じデータを抱えているのにこんなに小さくなるなんてー。
XMLのデータ
<?xml version="1.0" encoding="utf-8"?>
<管理情報>
<店舗情報>
<店舗名>本店</店舗名>
<店長>
<雇用形態>0</雇用形態>
<名前>森</名前>
<年齢>23</年齢>
<給与>250000</給与>
</店長>
<メンバー>
<雇用形態>1</雇用形態>
<名前>小森</名前>
<年齢>18</年齢>
<給与>190000</給与>
</メンバー>
<メンバー>
<雇用形態>0</雇用形態>
<名前>森</名前>
<年齢>23</年齢>
<給与>250000</給与>
</メンバー>
<メンバー>
<雇用形態>1</雇用形態>
<名前>大島</名前>
<年齢>54</年齢>
<給与>170000</給与>
</メンバー>
<メンバー>
<雇用形態>1</雇用形態>
<名前>中田</名前>
<年齢>35</年齢>
<給与>160000</給与>
</メンバー>
<メンバー>
<雇用形態>1</雇用形態>
<名前>篠田</名前>
<年齢>23</年齢>
<給与>180000</給与>
</メンバー>
<メンバー>
<雇用形態>1</雇用形態>
<名前>佐藤</名前>
<年齢>37</年齢>
<給与>160000</給与>
</メンバー>
<メンバー>
・・・
まとめ
MessagePackはデータをバイナリとして扱うので軽量かつ高速で使用できることがわかりました。データがバイナリで保存されるとメモ帳などでファイルを開いた時にわけわかめ状態ですが、テキストとして開く必要のない場合には導入してみるのも面白いです。
様々なプラットフォームに対応
MessagePackはRuby,Python,Perl,C/C++,Java,PHP,JavaScript,Objective-C,C#,etc..様々な言語で使用することができます。データのフォーマットとしては統一されているので、試していませんが互換性があるようですので、通信フォーマットとしても使用するのも面白いと思います。
あー、今度使ってみようっと。