MessagePack:一种高效的二进制序列化格式
对象序列化格式一般分成两类,分别是文本格式和二进制格式。典型的文本格式包括 XML 和 JSON。文本格式简单易用,编写和解析起来很容易,方便开发人员的使用。典型的二进制格式包括 Protocol Buffers 和 Apache Avro。二进制格式消息紧凑,所需的空间要少得多,需要工具的支持才能编写和解析。
本文介绍的 MessagePack 是另外一种二进制序列化格式。MessagePack 的提出者是 Sadayuki Furuhashi。Sadayuki 也是 Fluentd 的创建者,现在是 Treasure Data 的首席架构师。MessagePack 被用在 Redis、Fluentd、Treasure Data 和 Pinterest。
MessagePack 的两个基本概念是类型系统和格式。类型系统定义了所支持的类型,格式则定义了如何编码这些类型。MessagePack 的类型系统中包含下面的类型:
- Integer、Nil、Boolean、Float
- Raw 有子类型 String 和 Binary
- Array 表示数组
- Map 表示名值对
- Extension 表示扩展类型
MessagePack 允许应用通过扩展类型来创建自定义类型。每个扩展类型都由类型和数据两部分组成。应用的自定义类型可以使用的类型范围是 0 到 127。类型范围 -1 到 -128 则由 MessagePack 保留,用来添加预定义类型。目前只有一种预定义类型 Timestamp
,使用类型 -1。
在格式方面,MessagePack 通过不同的二进制字节模式来区分不同的类型。通过查看首字节的前缀,就可以判断出类型。
比如,nil
的对应的字节是 0xc0
,false
是 0xc2
,true
是 0xc3
。32位整数的前缀是 0xd2
,具体的格式如下所示:
+--------+--------+--------+--------+--------+
| 0xd2 |ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|
+--------+--------+--------+--------+--------+
复杂类型的格式也是类似。比如 map 16 可以表示最多 2^16 - 1 个名值对,前缀是 0xde
。 后面紧跟的是名值对的数量 N
,然后是 N * 2
个元素,按照先名称后值的方式依次排列。
当然了,作为使用者,我们并不需要关心 MessagePack 的格式细节。只需要使用类库来进行序列化即可。MessagePack 支持多种不同的编程语言。Java 可以使用 msgpack-java 库。
在进行序列化时,只需要创建一个 MessageBufferPacker
对象,再使用不同的 pack
方法来编码不同类型的数据。
在下面的代码中,依次编码了一个整数、一个字符串、一个数组和一个 map。
byte[] pack() throws IOException {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packInt(1)
.packString("hello world")
.packArrayHeader(2)
.packString("x")
.packString("y")
.packMapHeader(1)
.packString("name")
.packString("alex");
packer.close();
return packer.toByteArray();
}
序列化结果的长度仅为 29 个字节。
在进行反序列化时,需要创建一个 MessageUnpacker
对象,再使用不同的 unpack
方法来提取数据。
void unpack(byte[] data) throws IOException {
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data);
int id = unpacker.unpackInt();
String name = unpacker.unpackString();
int arrayLength = unpacker.unpackArrayHeader();
String[] array = new String[arrayLength];
for (int i = 0; i < arrayLength; ++i) {
array[i] = unpacker.unpackString();
}
int mapSize = unpacker.unpackMapHeader();
Map<String, String> map = new HashMap<>(mapSize);
for (int i = 0; i < mapSize; i++) {
map.put(unpacker.unpackString(), unpacker.unpackString());
}
unpacker.close();
System.out.printf("%d, %s, %s, %s%n", id, name, Arrays.toString(array),
map);
}
直接用 MessagePack 的底层 API 可能不太灵活。可以使用 MessagePack 提供的 Jackson 扩展库,以 Jackson 的 ObjectMapper 接口来使用。这样会简单不少。如下面的代码所示:
ObjectMapper objectMapper = new MessagePackMapper();
List<Object> list = new ArrayList<>();
list.add("Foo");
list.add("Bar");
list.add(42);
byte[] bytes = objectMapper.writeValueAsBytes(list);
List<Object> deserialized = objectMapper.readValue(bytes, new TypeReference<List<Object>>() {});
System.out.println(deserialized); // => [Foo, Bar, 42]
最后比较一下 MessagePack 与其他的序列化格式。与 JSON 相比,MessagePack 的编码结果更紧凑,生成的结果更小。比如下图中的数据,JSON 编码需要 27 个字节,MessagePack 只需要 18 个字节。
与 Protocol Buffers 相比,MessagePack 并没有使用 tag 来标识字段,保留了字段的名称。因此并不需要对消息进行定义,仅从数据自身就可以进行解码,序列化的结果也会比 Protocol Buffers 大。
从这个角度来说,MessagePack 更像是紧凑版本的 JSON,可以在某些场合替换 JSON。