MeteorCat / msgpack数据交换

Created Fri, 02 May 2025 22:45:31 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
1782 Words

这是在编写特定简单业务服务端的时候看到大部分都采用 protobuf, 但是需要配置环境利用本身 proto.exe 把数据结构打包引入.

实际上我不太喜欢用 protobuf 作为数据交换方案, 多端功能要同步文件再打包成序列化文件引入才能使用.

比较通用的方案就是采用 JSON 就能多平台直接传输, 但是解析性能和传输数据量对于高性能服务端来说都不能接受, 因此需要新的数据交换方案来支持 多端交换/性能高效/配置简洁.

在这种多种方案之中, 最后发现 MessagePack 这套数据交换方案满足我的要求.

传统的 JSON 方案主要的问题就像官网揭示的一样:

// 可以看到传统JSON的占用, 首先 '{' 和 '}' 包裹就占用大量数据(节点越多占用越多)
// 而 string 元素则是额外占用两个 '"' 包裹起来(节点越多占用越多)
{
  "id": 100,
  "msg": "hello"
}

这也是 JSON 长期被诟病的地方, 随着数据结构越复杂传输性能和效率也越低; 而 messagePack 则是采用新的压缩方式移除掉内部多余的占用数据.

另外 messagePack 解析处理简单粗暴, 不需要采用额外的二进制编译处理, 打开官网只能能看到所有平台例子

特别是对于 WebSocket 来说, 这种轻量级协议直接引入第三方库就能极其方便的使用; 在更新迭代多次之后 msgpack 越来越具有可用性, 尝试一次过后简直惊为天人.

JSON 兼容性直接保证 msgpack 也大部分兼容数据格式, 只需要像 JSON 一样处理.

更加粗暴的就是只需要 Msgpack.encode|Msgpack.decode 就能直接处理数据, 和JSON一样过于简单方便

Java

这里采用官方 msgpack-java 库处理就行了, 采用 maven 简单引入就行了:

<!-- 听说v7版本实现效率比v6版本更快, 可以按照官方引入 -->
<dependency>
    <groupId>org.msgpack</groupId>
    <artifactId>msgpack-core</artifactId>
    <version>0.9.9</version>
</dependency>

对于 Java17 因为涉及到操作内存功能, 需要对 jvm 追加配置:

--add-opens=java.base/java.nio=ALL-UNNAMED
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED

对于 Jackson 序列化官方提供了 jackson-databind 绑定方法

之后编写个测试单元

package com.fusion.game.common;

import org.junit.Test;
import org.msgpack.core.MessagePack;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * MessagePackage测试
 */
public class MessageUtilsTests {

    /**
     * 测试写入消息格式
     */
    @Test
    public void encodeData() {
        String msg = "Hello World!中文测试👿"; // 中文+emoji字符串
        int[] arrays = new int[]{1, 23, 4, 5};// 列表
        byte[] bytes = new byte[]{1, 2, 3, 4, 5}; // 字节数组
        Map<String, Object> maps = new LinkedHashMap<>(); // 对象组, 注意HashMap是无序的
        maps.put("id", 1001);
        maps.put("name", "MeteorCat");


        // 开始写入结构
        try (var packer = MessagePack.newDefaultBufferPacker()) {

            // 写入字符串
            packer.packString(msg);

            // 写入数组
            // 数组比较特殊, 需要先写入头长度再写入内容
            packer.packArrayHeader(arrays.length);
            for (int value : arrays) {
                packer.packInt(value);
            }

            // 写入字节数组其实也和数组类似
            // 内置方法集成可以直接写入二进制
            packer.packBinaryHeader(bytes.length);
            packer.writePayload(bytes);

            // 写入对象组, 和数组一样必须写入头长度
            // 注意: 对于对象组需要自己去判断序列化结构从而写入数据内容
            packer.packMapHeader(maps.size());
            for (Map.Entry<String, Object> entry : maps.entrySet()) {
                var key = entry.getKey();
                var value = entry.getValue();

                // 先把字符串的Key写入进去
                packer.packString(key);
                System.out.println("对象组KEY: " + key);

                // 自己去判断结构内容写入
                if (value instanceof Integer) {
                    packer.packInt((Integer) value);
                } else if (value instanceof String) {
                    packer.packString((String) value);
                }
            }


            // 最后处理输出二进制数据
            byte[] message = packer.toByteArray();

            // 直接测试打印内容
            System.out.print("msgpack 编码数据: ");
            System.out.println(Arrays.toString(message));

            // 最后输出以下内容
            // msgpack 编码数据: [-68, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, -28, -72, -83, -26, -106, -121, -26, -75, -117, -24, -81, -107, -16, -97, -111, -65, -108, 1, 23, 4, 5, -60, 5, 1, 2, 3, 4, 5, -126, -94, 105, 100, -51, 3, -23, -92, 110, 97, 109, 101, -87, 77, 101, 116, 101, 111, 114, 67, 97, 116]
        } catch (IOException e) {
            // 解析错误
            System.err.println(e.getMessage());
        }
    }


    /**
     * 解码数据
     */
    @Test
    public void decodeData() {
        // 获取上面的序列化数据
        byte[] bytes = new byte[]{-68, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, -28, -72, -83, -26, -106, -121, -26, -75, -117, -24, -81, -107, -16, -97, -111, -65, -108, 1, 23, 4, 5, -60, 5, 1, 2, 3, 4, 5, -126, -94, 105, 100, -51, 3, -23, -92, 110, 97, 109, 101, -87, 77, 101, 116, 101, 111, 114, 67, 97, 116};

        // 开始测试解码数据
        try (var unpacker = MessagePack.newDefaultUnpacker(bytes)) {
            // 注意解析必须按照编码时候的数据顺序来解码, 否则就代表结构本身是错误的
            // 比如编码首位是字符串, 则必须解码字符串
            var msg = unpacker.unpackString();
            System.out.printf("字符串: %s%n", msg);

            // int数组, 需要手动申请数组并生成
            var arraysLength = unpacker.unpackArrayHeader();
            var arrays = new int[arraysLength];
            for (int i = 0; i < arraysLength; i++) {
                arrays[i] = unpacker.unpackInt();
            }
            System.out.printf("Int数组: %s%n", Arrays.toString(arrays));

            // 字节流数组
            var bytesLength = unpacker.unpackBinaryHeader();
            var bytesData = new byte[bytesLength];
            for (int i = 0; i < bytesLength; i++) {
                bytesData[i] = unpacker.unpackByte();
            }
            System.out.printf("字节流数组: %s%n", Arrays.toString(bytesData));

            // 对象组其实也差不多
            // 但是需要注意: 对象组的解码更加复杂, 他需要对每个位置类型做单独处理
            var maps = new HashMap<String, Object>(unpacker.unpackMapHeader());


            // 需要对首位对象组数据 String -> Integer 导出
            var firstKey = unpacker.unpackString();
            var firstValue = unpacker.unpackInt();
            maps.put(firstKey, firstValue);

            // 次位 String -> String 导出
            var secondKey = unpacker.unpackString();
            var secondValue = unpacker.unpackString();
            maps.put(secondKey, secondValue);

            System.out.printf("对象组: %s%n", maps);

        } catch (IOException e) {
            // 解析错误
            System.err.println(e.getMessage());
        }
    }
}

最后得出序列化和反序列化数据内容:

## encodeData
对象组KEY: id
对象组KEY: name
msgpack 编码数据: [-68, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, -28, -72, -83, -26, -106, -121, -26, -75, -117, -24, -81, -107, -16, -97, -111, -65, -108, 1, 23, 4, 5, -60, 5, 1, 2, 3, 4, 5, -126, -94, 105, 100, -51, 3, -23, -92, 110, 97, 109, 101, -87, 77, 101, 116, 101, 111, 114, 67, 97, 116]

## decodeData
字符串: Hello World!中文测试👿
Int数组: [1, 23, 4, 5]
字节流数组: [1, 2, 3, 4, 5]
对象组: {name=MeteorCat, id=1001}

很久以前版本好像对于中文和特殊字符等其他内容有识别问题, 现在好像已经修复完成