官方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 specification
概括来讲就是:
我们看一下官方给出的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时就按我们自定义的结构去解析。具体怎么实现后面我们在代码示例的时候会讲到。
github地址
从这里也能看到它对各种语言的支持:c、java、ruby、python、php...
感兴趣的可以自己阅读,比较简单易懂,这里不再赘述,下面重点讲一下具体用法。
首先需要在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。
主要有两种用法:
通过 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、二进制数据的打包用法。
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 = 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中。搞定。那解包的时候怎么做呢,后面我们会讲到。两种用法与上面打包是对应的:
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。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 CREATOR = new Creator() {
@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更轻便更灵活的一种数据协议。