这是在编写特定简单业务服务端的时候看到大部分都采用 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}
很久以前版本好像对于中文和特殊字符等其他内容有识别问题, 现在好像已经修复完成