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