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阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 今天心情真的跌落倒谷底了,世界又黑暗了不少。外面还在淅淅沥沥的下着雨,天空也是暗沉的。我感觉到绝望的感觉,这条路真...
    渔鱼鱼阅读 275评论 0 0