Edited at

MessagePackバイナリを自分でデコードして読む


モチベーション

スマホゲームの通信を読んでいると、MessagePackをデコーダーなしで読めた方が効率が良いことがあるので、読んでいきたいと思います。


覚える仕様表

MessagePackの仕様表は40行くらいあるのですが、一部の型を読むのを諦めると、以下の2つの表にまとめられます。

最初の8bitが以下のとき
意味
読み方

0,1,2,3,4,5,6,7
0〜127の 1のInteger

そのまま

4F → 792

8
15要素までの Map

9〜16bit目が要素数(n)、
それ以降n*2バイトがMapの各要素
(Key,Valueの繰り返し)

81 01 02 → {1: 2}

9
15要素までの Array

9〜16bit目が要素数(n)、
それ以降nバイトがArrayの各要素

91 4F → [79]

a,b
31文字までの ASCII String

8〜16bit目が文字列長(n)、
それ以降nバイトが文字列の中身

A2 65 97 → "Aa"

e,f
-32〜-1の 負のInteger

そのまま

FE → -22

16bitが以下のとき
意味

c0
NULL

c1
(never used)

c2
TRUE

c3
FALSE

最初がc4〜cf及びdで始まる場合 は、16要素以上のArray/Mapや、Float、バイナリ(⊃ASCII以外の文字列)など、上の表の範囲を超えるデータを表現するために使用します。

スマホゲームの通信を見ていると、上の表の範囲を覚えておけばだいたいのことは読めることが多いため、ここでは上の9行の表のみ覚えることにして、実際の通信を読んでみます。


実際に読んでみた

以下は、いま適当に考えた、スマホゲーム通信でよくありそうなダミーのMessagePackバイナリ列です。

00000000  83 a2 6f 6b c3 a6 6d 65  74 68 6f 64 a7 4c 65 76  |..ok..method.Lev|

00000010 65 6c 55 70 a6 73 74 61 74 75 73 97 23 37 28 32 |elUp.status.#7(2|
00000020 32 5a cd 01 40 |2Z..@|

頭から読んでいきます。読んだ途中経過はJSONとして表記していきます。


  1. 最初のバイト 83 は3要素Mapであることがわかります。したがって、以降の要素6(=3*2)個分を読み、Map構造と解釈すればよいです。



    • {?:?, ?:?, ?:?}



  2. 次のバイト a2 は長さ2のStringです。先程読んだ3要素Mapの最初の要素のKeyはこの長さ2のStringとなります。



    • {"(長さ2の文字列?)":?, ?:?, ?:?}



  3. 続く2バイト 6f 6b はASCII文字として読めば良いので、 "ok" となります。これで3要素Mapの1要素目のKeyが確定したので、次のバイトからはその要素のValueとなります。



    • {"ok":?, ?:?, ?:?}



  4. 次のバイト c3 はTrueです。これで3要素MapのValueも確定したので、次のバイトからは3要素Mapの2要素目となります。



    • {"ok":True, ?:?, ?:?}



  5. 以下、2要素目、3要素目のKeyも同様に読んでいきます。すべて文字列で、以下のようになります。



    • {"ok":True, "method": "LevelUp", "status":?}



  6. 残りの 97 23 37 28 32 5a cd 01 40 のうち、最初の 97 は7要素Arrayです。


    • {"ok":True, "method": "LevelUp", "status":[(7要素Array?)]}



  7. その後の 23, 37, 28, 32 32 5a までは正の整数で、そのまま読めます。


    • {"ok":True, "method": "LevelUp", "status":[35, 55, 40, 50, 50, 90, ?]}



  8. 次の要素は cd から始まるため、表の範囲から外れており読めません。どうしても読みたい場合は大きい方の仕様表を読んで内容を確認しましょう。


    • 確認すると、 cd はuint 16型で、後ろ2つのバイトをそのまま数値として読めばよいので、 cd 01 40 は 320(0x140)となります。これを読むと、7要素Array、トップレベルの3要素Mapが全て読み終わり、以上でデコード終了となります。


    • {"ok":True, "method": "LevelUp", "status":[35, 55, 40, 50, 50, 90, 320]}3




まとめ

スマホゲームの通信を見る目的の多くは、通信の種類("BattleStart"とか、"GachaResult"とか)や整数値を見るためであることが多いので、案外上の表だけ覚えていれば実用に足ることが多いです4

また、まともな通信仕様であれば、MapのKey部分は32文字以下のASCII文字であることがほとんどだと思いますので[要出典]、知りたいデータの入ってそうなKeyを見つけ、そのValueを大きい方の仕様表を使って読めば、自力でデコードする量を最小限にしてMessagePackを読むことができるかと思います。


参考

MessagePackの仕様表

https://github.com/msgpack/msgpack/blob/master/spec.md#formats

ダミーバイナリ列を作るのに使ったやつ:

https://github.com/ludocode/msgpack-tools





  1. ここでの 正 は0を含むものとします 



  2. バイナリは16進表記、デコード後の整数は10進表記をすることにします。 



  3. この"status"はピカチュウの種族値から持ってきた値です。 



  4. 実際に読んでみた例のように、正の整数が127からはみ出ることがよくあるので、 128以上の大きな数字を扱うことが多いゲームの通信を見る場合は、 cc(uint 8), cd (uint16)を追加で覚えていても良いかもしれません。