什么是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
概括來講就是:
- true、false 之類的:這些太簡(jiǎn)單了,直接給1個(gè)字節(jié),(0xc3 表示true,0xc2表示false)
- 不用表示長(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é)表示。
- 不定長(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的字符串大小。
- 高級(jí)結(jié)構(gòu):MAP結(jié)構(gòu),就是k-v 結(jié)構(gòu)的數(shù)據(jù),和數(shù)組差不多,加1~4個(gè)字節(jié)表示后面有多少個(gè)項(xiàng)
- 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é)議。