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 的对应的字节是 0xc0false0xc2true0xc3。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 个字节。

MessagePack 与 JSON 比较

与 Protocol Buffers 相比,MessagePack 并没有使用 tag 来标识字段,保留了字段的名称。因此并不需要对消息进行定义,仅从数据自身就可以进行解码,序列化的结果也会比 Protocol Buffers 大。

从这个角度来说,MessagePack 更像是紧凑版本的 JSON,可以在某些场合替换 JSON。

版权所有 © 2024 灵动代码