MessagePack簡(jiǎn)介及使用

什么是MessagePack

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


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

MessagePack的壓縮原理

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

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

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



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

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

怎么把tabs中的子數(shù)據(jù)作為一個(gè)整體寫入itemdata這個(gè)結(jié)構(gòu)中呢?itemdata又怎么寫入它的上層數(shù)據(jù)結(jié)構(gòu)data中?這時(shí)Ext出馬了。我們可以自定義一種數(shù)據(jù)類型,指定它的Type值,當(dāng)解析遇到這個(gè)type時(shí)就按我們自定義的結(jié)構(gòu)去解析。具體怎么實(shí)現(xiàn)后面我們?cè)诖a示例的時(shí)候會(huì)講到。

MessagePack的源碼

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

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這個(gè)facade獲取用戶可用的對(duì)象packer和unpacker。

1. 數(shù)據(jù)打包


主要有兩種用法:

  • 通過 MessageBufferPacker將數(shù)據(jù)打包到內(nèi)存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(); 

以上分別展示了對(duì)基本數(shù)據(jù)類型、array數(shù)組、map、二進(jìn)制數(shù)據(jù)的打包用法。

  • 通過 MessagePacker將數(shù)據(jù)直接打包輸出流
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));
// 以下是對(duì)自定義數(shù)據(jù)類型的打包
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將自定義數(shù)據(jù)類型的type值和它的長(zhǎng)度寫入,這里指定這段數(shù)據(jù)的type=1,長(zhǎng)度就是轉(zhuǎn)為二進(jìn)制數(shù)據(jù)后的長(zhǎng)度,這里官方demo里有個(gè)錯(cuò)誤,寫了固定長(zhǎng)度10,其實(shí)是有問題的,這里進(jìn)行了修正寫入extData的實(shí)際長(zhǎng)度。然后用writePayload方法將byte[]數(shù)據(jù)寫入。結(jié)束??赡苓@個(gè)Demo的展示還有點(diǎn)不太好理解,我們就上面的json樣式進(jìn)行進(jìn)一步說明:假設(shè)我要將tabs下的數(shù)據(jù)樣式定義為一個(gè)擴(kuò)展類型,怎么去寫呢?
首先定義一個(gè)這樣的數(shù)據(jù)結(jié)構(gòu):

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

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

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

然后構(gòu)造MessagePacker進(jìn)行寫入

    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對(duì)象打包成二進(jìn)制數(shù)據(jù),然后我們將這個(gè)二進(jìn)制數(shù)據(jù)寫到packer中。搞定。那解包的時(shí)候怎么做呢,后面我們會(huì)講到。
這樣通過自定義數(shù)據(jù)結(jié)構(gòu)層層打包就完美解決了上面關(guān)于怎么將數(shù)據(jù)打包為復(fù)雜json樣式的問題了。
必須注意打包結(jié)束后必須進(jìn)行close,以結(jié)束此次buffer操作或者關(guān)閉輸出流。

2. 數(shù)據(jù)解包


兩種用法與上面打包是對(duì)應(yīng)的:

  • 直接對(duì)二進(jìn)制數(shù)據(jù)解包
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();

需要注意的是解包順序必須與打包順序一致,否則會(huì)出錯(cuò)。也就是說協(xié)議格式的維護(hù)要靠?jī)啥耸謱懘a進(jìn)行保證,而這是很不安全的。

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

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

3. 其他雜談

如果想省去如此繁瑣的pack、unpack動(dòng)作,而又想用messagepack,可以做到么?當(dāng)然可以,我們可以利用java bean的序列化功能,將對(duì)象序列化為二進(jìn)制,然后整個(gè)寫入到messagepack中。
比如以上的TabsJson對(duì)象,在android中我們實(shí)現(xiàn)Parcelable接口以達(dá)到序列化的目的

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();
//先寫入數(shù)據(jù)長(zhǎng)度
packer.packInt(bytes.length);
//寫入二進(jìn)制數(shù)據(jù)
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();

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

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

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

一句話總結(jié)一下Messagepack

簡(jiǎn)單好用,掌握原理后可以想怎么用怎么用。是比Json更輕便更靈活的一種數(shù)據(jù)協(xié)議。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,626評(píng)論 18 399
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,295評(píng)論 0 17
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,769評(píng)論 25 709
  • 今天心情真的跌落倒谷底了,世界又黑暗了不少。外面還在淅淅瀝瀝的下著雨,天空也是暗沉的。我感覺到絕望的感覺,這條路真...
    漁魚魚閱讀 298評(píng)論 0 0

友情鏈接更多精彩內(nèi)容