MessagePack簡介及使用

什么是MessagePack

官方msgpack官網用一句話總結:
It’s like JSON.
but fast and small.
簡單來講,它的數據格式與json類似,但是在存儲時對數字、多字節字符、數組等都做了很多優化,減少了無用的字符,二進制格式,也保證不用字符化帶來額外的存儲空間的增加。以下是官網給出的簡單示例圖:


圖上這個json長度為27字節,但是為了表示這個數據結構,它用了9個字節(就是那些大括號、引號、冒號之類的,他們是白白多出來的)來表示那些額外添加的無意義數據。msgpack的優化在圖上展示的也比較清楚了,省去了特殊符號,用特定編碼對各種類型進行定義,比如上圖的A7,其中前四個bit A就是表示str的編碼,而且它表示這個str的長度只用半個字節就可以表示了,也就是后面的7,因此A7的意思就是表示后面是一個7字節長度的string。
有的同學就會問了,對于長度大于15(二進制1111)的string怎么表示呢?這就要看messagepack的壓縮原理了。

MessagePack的壓縮原理

核心壓縮方式可參看官方說明messagepack specification
概括來講就是:

  1. true、false 之類的:這些太簡單了,直接給1個字節,(0xc3 表示true,0xc2表示false)
  2. 不用表示長度的:就是數字之類的,他們天然是定長的,是用一個字節表示后面的內容是什么,比如用(0xcc 表示這后面,是個uint 8,用oxcd表示后面是個uint 16,用 0xca 表示后面的是個float 32)。對于數字做了進一步的壓縮處理,根據大小選擇用更少的字節進行存儲,比如一個長度<256的int,完全可以用一個字節表示。
  3. 不定長的:比如字符串、數組、二進制數據(bin類型),類型后面加 1~4個字節,用來存字符串的長度,如果是字符串長度是256以內的,只需要1個字節,MessagePack能存的最長的字符串,是(2^32 -1 ) 最長的4G的字符串大小。
  4. 高級結構:MAP結構,就是k-v 結構的數據,和數組差不多,加1~4個字節表示后面有多少個項
  5. Ext結構:表示特定的小單元數據。也就是用戶自定義數據結構。

我們看一下官方給出的stringformat示意圖



對于上面的問題,一個長度大于15(也就是長度無法用4bit表示)的string是這么表示的:用指定字節0xD9表示后面的內容是一個長度用8bit表示的string,比如一個160個字符長度的字符串,它的頭信息就可以表示為D9A0。
這里值得一提的是Ext擴展格式,正是這種結構才保證了messagepack的完備性,因為實際的數據接口中自定義結構是非常常見的,簡單的已知數據類型和高級結構map、array等并不能滿足需求,因此需要一個擴展格式來與之配合。比如一個下面的接口格式:

{
  "error_no":0,
  "message":"",
  "result":{
    "data":[
      {
        "datatype":1,
        "itemdata":
            {//共有字段45個
              "sname":"\u5fae\u533b",
              "packageid":"330611",
              …
              "tabs":[
                        {
                          "type":1,
                          "f":"abc"
                        },
                        …
              ]
            }
      },
      …
    ],
    "hasNextPage":true,
    "dirtag":"soft"
  }
}

怎么把tabs中的子數據作為一個整體寫入itemdata這個結構中呢?itemdata又怎么寫入它的上層數據結構data中?這時Ext出馬了。我們可以自定義一種數據類型,指定它的Type值,當解析遇到這個type時就按我們自定義的結構去解析。具體怎么實現后面我們在代碼示例的時候會講到。

MessagePack的源碼

github地址
從這里也能看到它對各種語言的支持:c、java、ruby、python、php...
感興趣的可以自己閱讀,比較簡單易懂,這里不再贅述,下面重點講一下具體用法。

android studio中如何使用MessagePack

首先需要在app的gradle腳本中添加依賴

compile 'org.msgpack:msgpack-core:0.8.11'

java版本用法的sample可以在源碼的/msgpack-java/msgpack-core/src/test/java/org/msgpack/core/example/MessagePackExample.java中看到。
值得一提的是官方的說明文檔還停留在1.x版本,建議大家直接去看最新demo。
通過MessagePack這個facade獲取用戶可用的對象packer和unpacker。

1. 數據打包


主要有兩種用法:

  • 通過 MessageBufferPacker將數據打包到內存buffer中
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
        packer
                .packInt(1)
                .packString("leo")
        // pack arrays
        int[] arr = new int[] {3, 5, 1, 0, -1, 255};
        packer.packArrayHeader(arr.length);
        for (int v : arr) {
            packer.packInt(v);
        }
        // pack map (key -> value) elements
        packer.packMapHeader(2); // the number of (key, value) pairs
        // Put "apple" -> 1
        packer.packString("apple");
        packer.packInt(1);
        // Put "banana" -> 2
        packer.packString("banana");
        packer.packInt(2);

        // pack binary data
        byte[] ba = new byte[] {1, 2, 3, 4};
        packer.packBinaryHeader(ba.length);
        packer.writePayload(ba);

        packer.close(); 

以上分別展示了對基本數據類型、array數組、map、二進制數據的打包用法。

  • 通過 MessagePacker將數據直接打包輸出流
File tempFile = File.createTempFile("target/tmp", ".txt");
tempFile.deleteOnExit();
// Write packed data to a file. No need exists to wrap the file stream with BufferedOutputStream, since MessagePacker has its own buffer
MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile));
// 以下是對自定義數據類型的打包
byte[] extData = "custom data type".getBytes(MessagePack.UTF8);
packer.packExtensionTypeHeader((byte) 1, extData.length());  // type number [0, 127], data byte length
packer.writePayload(extData);
packer.close();

首先通過packExtensionTypeHeader將自定義數據類型的type值和它的長度寫入,這里指定這段數據的type=1,長度就是轉為二進制數據后的長度,這里官方demo里有個錯誤,寫了固定長度10,其實是有問題的,這里進行了修正寫入extData的實際長度。然后用writePayload方法將byte[]數據寫入。結束??赡苓@個Demo的展示還有點不太好理解,我們就上面的json樣式進行進一步說明:假設我要將tabs下的數據樣式定義為一個擴展類型,怎么去寫呢?
首先定義一個這樣的數據結構:

public class TabsJson {
    public int type;
    public String f = "";
}

然后指定TabsJson對象的type ExtType.TYPE_TAB=2,官方對自定義數據類型的限制是0~127。
然后對TabsJson對象進行初始化和賦值:

TabsJson tabsjson = new TabsJson();
tabsjson.type = 199;
tabsjson.f = "abc";

然后構造MessagePacker進行寫入

    private static void packTabJson(TabsJson tabsJson, MessagePacker packer) throws IOException {
        MessageBufferPacker packer1 = MessagePack.newDefaultBufferPacker();
        packer1.packInt(tabsJson.type);
        packer1.packString(tabsJson.f);
        int l = packer1.toByteArray().length;
        packer.packExtensionTypeHeader(ExtType.TYPE_TAB,l);
        packer.writePayload(packer1.toByteArray());
        packer1.close();
    }

packer1的作用就是將tabsjson對象打包成二進制數據,然后我們將這個二進制數據寫到packer中。搞定。那解包的時候怎么做呢,后面我們會講到。
這樣通過自定義數據結構層層打包就完美解決了上面關于怎么將數據打包為復雜json樣式的問題了。
必須注意打包結束后必須進行close,以結束此次buffer操作或者關閉輸出流。

2. 數據解包


兩種用法與上面打包是對應的:

  • 直接對二進制數據解包
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes);
        int id = unpacker.unpackInt();             // 1
        String name = unpacker.unpackString();     // "leo"
        int numPhones = unpacker.unpackArrayHeader();  // 2
        String[] phones = new String[numPhones];
        for (int i = 0; i < numPhones; ++i) {
            phones[i] = unpacker.unpackString();   // phones = {"xxx-xxxx", "yyy-yyyy"}
        }
        int maplen = unpacker.unpackMapHeader();
        for (int j = 0; j < mapen; j++) {
             unpacker.unpackString();
             unpacker.unpackInt();
        }           
        unpacker.close();

需要注意的是解包順序必須與打包順序一致,否則會出錯。也就是說協議格式的維護要靠兩端手寫代碼進行保證,而這是很不安全的。

  • 對輸入流進行解包
 FileInputStream fileInputStream = new FileInputStream(new File(filepath));
 MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(fileInputStream);
//先將自定義數據的消息頭讀出
ExtensionTypeHeader et = unpacker.unpackExtensionTypeHeader();
//判斷消息類型
if (et.getType() == (ExtType.TYPE_TAB)) {
    int lenth = et.getLength();
    //按長度讀取二進制數據
    byte[] bytes = new byte[lenth];
    unpacker.readPayload(bytes);
    //構造tabsjson對象
    TabsJson tab = new TabsJson();
    //構造unpacker將二進制數據解包到java對象中
    MessageUnpacker unpacker1 = MessagePack.newDefaultUnpacker(bytes);
    tab.type = unpacker1.unpackInt();
    tab.f = unpacker1.unpackString();
    unpacker1.close();
}
unpacker.close();

以上例子展示了對自定義數據類型的完整解包過程,最后不要忘記關閉unpacker。
除此之外用戶還可以自定義packconfig和unpackconfig,指定打包和解包時的配置,比如內存緩存byte[]數據大小等等。

3. 其他雜談

如果想省去如此繁瑣的pack、unpack動作,而又想用messagepack,可以做到么?當然可以,我們可以利用java bean的序列化功能,將對象序列化為二進制,然后整個寫入到messagepack中。
比如以上的TabsJson對象,在android中我們實現Parcelable接口以達到序列化的目的

public class TabsJson implements Parcelable {
    public int type;
    public String f = "";
    public TabsJson () {
    }
    protected TabsJson(Parcel in) {
        this.type = in.readInt();
        this.f = in.readString();
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.type);
        dest.writeString(this.f);
    }
    @Override
    public int describeContents() {
        return 0;
    }
    public static final Creator<TabsJson> CREATOR = new Creator<TabsJson>() {
        @Override
        public TabsJson createFromParcel(Parcel in) {
            return new TabsJson(in);
        }
        @Override
        public TabsJson[] newArray(int size) {
            return new TabsJson[size];
        }
    };
}

打包和解包過程是這樣的

MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
Parcel pc = Parcel.obtain();
tabsjson.writeToParcel(pc, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
byte[] bytes = pc.marshall();
//先寫入數據長度
packer.packInt(bytes.length);
//寫入二進制數據
packer.writePayload(bytes);
packer.close();
pc.recycle();
//解包
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(packer.toByteArray());
byte[] bytes1 = new byte[unpacker.unpackInt()];
unpacker.readPayload(bytes1);
Parcel pp = Parcel.obtain();
pp.unmarshall(bytes1,0,bytes1.length);
pp.setDataPosition(0);
TabsJson ij = TabsJson.CREATOR.createFromParcel(pp);
pp.recycle();
unpacker.close();

這種方式雖然省去了自己手寫打包和解包的過程,但是不推薦使用。
筆者對第一部分示例的json數據,同一個itemdata數據段兩種方式打包后文件大小對比如下:

parcel方式 直接操作 Json數據
數據大小(byte) 3619 2644 4090

可見parcel方式在壓縮效率上比原始的json數據格式并無較大提升,因此不建議使用。

一句話總結一下Messagepack

簡單好用,掌握原理后可以想怎么用怎么用。是比Json更輕便更靈活的一種數據協議。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,829評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,737評論 18 399
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,495評論 0 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,761評論 25 708
  • 今天心情真的跌落倒谷底了,世界又黑暗了不少。外面還在淅淅瀝瀝的下著雨,天空也是暗沉的。我感覺到絕望的感覺,這條路真...
    漁魚魚閱讀 281評論 0 0