MessagePack - 简介及使用

简介: 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更轻便更灵活的一种数据协议。

目录
相关文章
|
NoSQL Redis Docker
【从零开始】Docker Desktop:听说你小子要玩我
本文旨在帮助读者重新掌握Docker知识,特别是在Windows 10环境下安装与使用Docker Desktop的过程。文章详细介绍了从下载安装到配置镜像源的具体步骤,并通过Redis实例演示了如何拉取镜像和启动容器。
12331 1
【从零开始】Docker Desktop:听说你小子要玩我
|
存储 Linux 数据安全/隐私保护
安装部署milvus单机版(快速体验)
安装部署milvus单机版(快速体验)
5751 0
|
7月前
|
人工智能 安全 Linux
2026年OpenClaw(Clawdbot) Linux部署:本地搭建+ZeroNews访问+云上部署教程
2026年初,OpenClaw(前身为Clawdbot)凭借“私有化运行+全权限实操”的创新定位,迅速成为AI领域的现象级开源项目。这款被称为“真正能做实事的AI”的工具,打破了传统聊天机器人的功能局限,可在本地设备或服务器上独立运行,通过WhatsApp、Telegram等常用聊天软件接收指令,完成文件读写、邮件管理、系统运维等实操任务,所有数据本地存储,隐私安全性拉满。
5460 9
|
12月前
|
XML Java Nacos
Spring Boot 整合Nacos 版本兼容适配 史上最详细文档
本文介绍SpringBoot整合Nacos的完整流程,涵盖Nacos下载安装、配置中心与服务发现集成、版本兼容性问题及实战配置。重点解决SpringBoot 3.3.0与Nacos版本适配难题,推荐使用Spring Cloud Alibaba方案,并提供项目开源地址供参考学习。
|
JSON 缓存 Java
优雅至极!Spring Boot 3.3 中 ObjectMapper 的最佳实践
【10月更文挑战第5天】在Spring Boot的开发中,ObjectMapper作为Jackson框架的核心组件,扮演着处理JSON格式数据的核心角色。它不仅能够将Java对象与JSON字符串进行相互转换,还支持复杂的Java类型,如泛型、嵌套对象、集合等。在Spring Boot 3.3中,通过优雅地配置和使用ObjectMapper,我们可以更加高效地处理JSON数据,提升开发效率和代码质量。本文将从ObjectMapper的基本功能、配置方法、最佳实践以及性能优化等方面进行详细探讨。
1465 2
|
Docker 容器
Docker安装及镜像源修改
本文介绍了Docker的安装过程和如何修改Docker镜像源以加速下载。包括更新系统包、安装所需软件包、设置yum源、安装Docker以及验证安装是否成功。接着,提供了修改Docker镜像源的步骤,包括创建配置文件、编辑配置文件以设置镜像加速地址,并提供了几个常用的国内镜像源地址。最后,通过重启Docker服务和检查配置是否生效来完成镜像源的修改。
Docker安装及镜像源修改
|
关系型数据库 MySQL Docker
docker环境下mysql镜像启动后权限更改问题的解决
在Docker环境下运行MySQL容器时,权限问题是一个常见的困扰。通过正确设置目录和文件的权限,可以确保MySQL容器顺利启动并正常运行。本文提供了多种解决方案,包括在主机上设置正确的权限、使用Dockerfile和Docker Compose进行配置、在容器启动后手动更改权限以及使用 `init`脚本自动更改权限。根据实际情况选择合适的方法,可以有效解决MySQL容器启动后的权限问题。希望本文对您在Docker环境下运行MySQL容器有所帮助。
3121 1

热门文章

最新文章