协议载体:com.huang.buffer.util.ByteTypeUtil
配套产物:com.huang.buffer.core.MessageMetaData
反序列化入口:GenerateMessageDataHandler.channelRead → ByteTypeUtil.handleLineBytes(统一入口)
版本:2026-08-23 修订(2026-08-15 增补嵌套 LIST 支持;2026-01-31 初版实现)


一、协议概述

Lemon-Protocol 是一种基于类型标记 + 长度前缀的紧凑型二进制序列化协议,用于把一组 Map<String, Object> 参数(单条)或一组同构的行记录(多条,形如关系表的列式压缩)编码为字节流,支持跨 TCP 长连接传输,并可在环形缓冲(RingBuffer/Disruptor)内作为事件载荷零拷贝传递。

协议核心设计目标:

  1. 极致紧凑:头部只保留字段个数、类型表、长度表,键名只在消息尾部出现一次,避免 JSON 式重复键名带来的体积膨胀。
  2. 批量压缩:多行消息把"类型表/长度表/名称表"去重为一份,数据区按行列矩阵排布,适合同构批量数据。
  3. 类型自描述:每个字段带 1 字节类型标记,反序列化时无需外部 schema 即可还原 10 种基础类型(含嵌套 LIST 多行结果集)。
  4. 流式边界:每条消息以 \n 结尾,可作为 Socket 流中多条消息的切分点。

二、类型系统(Type 枚举)

协议内置 10 种可传输类型 + 1 种兜底类型。标记字节为类型名首字母的 ASCII 码

类型 标记字节 ASCII 字符 值占用字节数 二进制编码
BYTE 98 b 1 原样 1 字节
SHORT 115 s 2 大端序 2 字节
CHAR 99 c 1 取低 8 位(仅单字节字符)
FLOAT 102 f 4 IEEE 754 大端序 4 字节
DOUBLE 100 d 8 IEEE 754 大端序 8 字节
INTEGER 105 i 4 大端序 4 字节
LONG 108 l 8 大端序 8 字节
BOOLEAN 66 B 1 1 字节,非 0 即 true
STRING 83 S 变长(UTF-8) UTF-8 字节序列;单行长度记录于长度表,多行内嵌 2 字节长度前缀
LIST 76 L 变长 2 字节大端嵌套块长度前缀 + 递归嵌套帧(多行结果集,见 3.3)
UNSUPPORTED 0 反序列化时返回 null

设计细节:boolean 用小写 b 会与 byte(98)冲突,因此用大写 B(66);string 用小写 s 会与 short(115)冲突,因此用大写 S(83);list 用小写 l 会与 long(108)冲突,因此用大写 L(76)。


三、消息格式

协议存在单行消息多行消息两种变体,二者均共享"类型表 + 长度表 + 值区 + 尾部名称区"的骨架,区别仅在头部。

3.1 单行消息格式(generateBytes;解析端保留但未接入发送链路)

字节偏移 长度(字节) 内容 / 定义 计算规则
字段数 N [0] 1 列数,范围 0~255;N=0 时直接返回空字节数组 类型区 / 长度区 / 值区的规模基准
类型区 [1, 1+N) N 每列 1 字节类型标记(见 二、类型系统) 起点 = 1;长度 = N
长度区 [1+N, 1+3N) 2N 每列 2 字节大端序长度,容量 0~65535(STRING 可超 255) 起点 = 1+N;长度 = 2N
值区 [1+3N, 1+3N+Σlen) Σlen 按列顺序拼接各列值:定长类型直接复制;STRING 按长度表写 UTF-8 字节 起点 = 1+3N;长度 = Σlen(各列长度之和)
名称区 值区之后 不定 key1|key2|…|keyN\n(UTF-8,\n 结尾作消息终止边界) 紧随值区;长度 = 键名总字节数 + 1

单条消息总长度 = 1 + N + 2N + Σlen + 名称区长度

  • 一号域(字段数 N):1 字节,取值范围 0~255,N=0 时直接返回空字节数组。
  • 二号域(类型区):N 个字节,按列顺序记录每列的类型标记。
  • 三号域(长度区):N×2 字节,按列顺序记录每列值的字节长度(大端序),容量 0~65535,因此字符串长度允许超过 255。
  • 四号域(值区):按列顺序拼接各列值,定长类型直接复制,STRING 按记录长度写入 UTF-8 字节。
  • 名称区:各列键名以 | 连接,末尾追加换行符 \n(作为消息终止边界)。

3.2 多行消息格式(generateLineBytes / handleLineBytes

字节偏移 长度(字节) 内容 / 定义 计算规则
一号域 lineMark [0] 1 'Y'(89)=多行,'N'(78)=单行 行数 >1 写 'Y',否则写 'N'
二号域 lineNumber [1, 3) 2 行数 M,大端序,上限 65535;单行时被忽略 M = list.size();解码时 'N' 按 1 行处理
三号域 列头数 N [3] 1 列数,范围 0~255 N = 首行列数
四号域 类型区 [4, 4+N) N 每列 1 字节类型标记;仅首行写入,后续行复用 起点 = 4;长度 = N
五号域 长度区 [4+N, 4+3N) 2N 定长列写字节长度(大端);STRING/LIST 列写 0(实际长度内嵌于值区);仅首行写入 起点 = 4+N;长度 = 2N
六号域 值区 [4+3N, 4+3N+Σlen) Σlen 行主序:先第 1 行 N 列,再第 2 行 N 列…;定长列按宽度平铺;STRING 列每行 = 2 字节大端长度前缀 + UTF-8(同列行内变长);LIST 列每行 = 2 字节大端嵌套块长度 + 嵌套帧 起点 = 4+3N;Σlen = Σ(所有行所有列):定长按类型宽、STRING 按 2 + 字节数、LIST 按 2 + 嵌套块字节数
名称区 值区之后 不定 首行键名 key1|…|keyN\n(UTF-8,\n 结尾作消息终止边界) 紧随值区;长度 = 键名总字节数 + 1

多行消息总长度 = 4 + N + 2N + Σlen + 名称区长度

  • 一号域(lineMark):1 字节,'Y'(89)表示多行、'N'(78)表示单行。
  • 二号域(lineNumber):2 字节大端序行数,上限 2^16 - 1 = 65535 行;单行时该域被忽略,解码时按 1 行处理。
  • 三号域(parameterNumber):1 字节列头数,上限 255 列;单行/多行通用。
  • 四号域(类型区):N 个字节,仅首行写入,后续行复用同一类型表。
  • 五号域(长度区):N×2 字节,仅首行写入,后续行复用。定长列写入其字节长度(大端序);STRING/LIST 列长度表置 0(实际长度内嵌于值区,见下)。
  • 六号域(值区)行主序(row-major)——先排第一行的 N 列,再排第二行的 N 列,以此类推。定长列按各自字节宽度平铺;STRING 列每行以 2 字节大端长度前缀开头,后跟 UTF-8 字节,因此同一 String 列在不同行允许长度不同(行内变长),单值上限 65535 字节;LIST 列每行以 2 字节大端嵌套块长度前缀开头,后跟嵌套帧字节(见 3.3)。
  • 名称区:取首行键名,| 连接,以 \n 结尾。

3.3 嵌套块(LIST 字段,2026-08-23 新增)

LIST 列的值是一个嵌套多行结果集,在值区内按如下布局递归编码:

[2 字节大端 nestedLen][嵌套帧字节]
  • 嵌套帧:递归调用 generateLineBytes(toRows(list, columnName), depth + 1, false) 的完整产物,与 3.2 的多行帧同构(4 字节头 + 类型区 + 长度区 + 值区 + 名称区),但末尾不带 \n——若带换行符,会被外层帧分隔符(DelimiterBasedFrameDecoder(65535, "\n"))误切成两条消息。
  • 空列表nestedLen = 0(嵌套帧为 0 字节),解码端返回 Collections.emptyList()
  • 标量列表(如 List<Integer>):自动包装为"单列多行"嵌套帧,列名沿用外层字段名,业务侧可按该列名识别。
  • 深度上限:递归层级上限 MAX_NEST_DEPTH = 8(根帧 depth = 0),超限时序列化/反序列化均抛 IllegalStateException,防止栈溢出与恶意报文攻击。
  • 可递归嵌套:嵌套帧自身可再含 LIST 列,支持任意层级的树状 / 多级主从结果集(受 8 层上限约束)。

3.4 空消息

parameters 为空(单行)或 list 为空(多行)时,序列化直接返回 new byte[0]

3.5 字节序与字符集

  • 所有多字节数值均为大端序(Big-Endian,网络序)
  • STRING 与名称区一律使用 UTF-8 编码。

四、序列化过程(编码)

4.1 单行:generateBytes(Map<String, Object>)

generateBytes(map)
 ├─ map 为空 → 返回 new byte[0]
 ├─ generateParameterLength(map)   → 构造头部 + 值区字节
 │    ├─ N = map.size(),总长度 = 1 + N + 2N + Σ(各值长度)
 │    ├─ bytes[0] = N
 │    ├─ 逐列写入:类型标记(Type.xxx.type())
 │    ├─ 逐列写入:长度值(2 字节大端序写入,index 每次 +2)
 │    ├─ 逐列写入:值本体(Integer/Long/Double/Float/Short 用 Netty Unpooled.copyXxx 大端复制;
 │    │             CHAR/BYTE/BOOLEAN 直接写 1 字节;STRING 写 UTF-8 字节)
 │    └─ 返回头部+值区字节
 ├─ generateParameterNameBytes(map) → "key1|key2|…|keyN" + "\n" 的 UTF-8 字节
 └─ Unpooled.wrappedBuffer(头部+值区, 名称区) 合并为完整消息

4.2 多行:generateLineBytes(List<Map<String, Object>>)

generateLineBytes(list, depth = 0, appendTerminator = true)
 ├─ depth > MAX_NEST_DEPTH(8) → 抛 IllegalStateException(深度超限)
 ├─ list 为空 → 返回 new byte[0]
 ├─ expandNestedFields(list) → ① 校验行间 schema 一致(fail-fast,见 6.7)
 │    ② 逐行逐列预序列化 LIST 列 → nestedCache[r][c] = 嵌套帧字节
 │       嵌套帧 = generateLineBytes(toRows(list, columnName), depth + 1, appendTerminator = false)
 │       └─ appendTerminator = false:嵌套帧末尾不带换行符,防被帧分隔符误切
 ├─ generateLineParameterLength(list, nestedCache) → 构造头部 + 值区字节
 │    ├─ lineNumber = list.size(),N = 首行列数
 │    ├─ lineMark = lineNumber > 1 ? 'Y' : 'N'
 │    ├─ 总长度 = 4 + N + 2N + Σ(遍历所有行所有列):
 │    │    定长列按类型字节宽累加;STRING 列按 2 + UTF-8 字节数累加(含内嵌前缀);
 │    │    LIST 列按 2 + 嵌套帧字节数累加(含内嵌前缀)
 │    ├─ bytes[0] = lineMark;bytes[1..2] = lineNumber(大端序)
 │    ├─ bytes[3] = N
 │    ├─ 外层循环行 i,内层循环列 v:
 │    │    ├─ i == 0 时写入类型表与长度表(定长列写真实长度、STRING/LIST 置 0;后续行跳过)
 │    │    └─ 每行每列都写值本体:定长列同单行规则;
 │    │       STRING 先写 2 字节大端长度前缀,再写 UTF-8 字节(行内变长的关键);
 │    │       LIST 先写 2 字节大端嵌套帧长度,再写 nestedCache 中的嵌套帧字节
 │    └─ 返回头部+值区字节
 └─ 名称区取首行:generateParameterNameBytes(list.get(0), appendTerminator),合并返回

4.3 各类型值编码对照

Java 类型 写入方式 占用
Integer Unpooled.copyInt 大端 4
Long Unpooled.copyLong 大端 8
Double Unpooled.copyDouble 大端 8
Float Unpooled.copyFloat 大端 4
Short Unpooled.copyShort 大端 2
Character (byte) value 取低 8 位 1
Byte (byte) value 1
Boolean Unpooled.copyBoolean 1
String UTF-8 字节序列 变长(≤65535)
List<?> 2 字节大端嵌套块长度 + 递归嵌套帧 变长(受帧上限约束)

五、反序列化过程(解码)

5.1 单行:1 字节头格式解析(历史保留,未接入发送链路)

说明:单行 1 字节头格式(generateBytes 产物)的解析器仅以私有方法保留在 GenerateMessageDataHandler 中,当前未接入发送链路。统一入口为 5.2 的 handleLineBytes——单行消息以 generateLineBytes(List.of(map)) 发送(4 字节头), 服务端收到后按 1 行处理。

handleBytes(bytes)   // 1 字节头:N + 类型区 + 长度区 + 值区 + 名称区
 ├─ N = bytes[0];N == 0 → 返回空列表
 ├─ typeIndex = 1,index = 1 + N,offset = 1 + 3N
 ├─ while (index ≤ 3N)  逐列:
 │    ├─ length = (bytes[index] << 8) | bytes[index+1]    ← 2 字节大端长度
 │    ├─ type = getType(bytes[typeIndex])
 │    ├─ 从 offset 拷贝 length 字节 → getValue(type, copy) 还原 Object
 │    ├─ 构造 MessageMetaData(type, value);offset += length;index += 2;typeIndex += 1
 ├─ 尾部解析名称:byteBuf.toString(offset, 剩余, UTF-8) → split("\\|") → 逐列 setName
 └─ 返回 List<MessageMetaData>(一列一个元素)

5.2 多行:handleLineBytes(byte[])(统一反序列化入口)

handleLineBytes(bytes, depth = 0)
 ├─ depth > MAX_NEST_DEPTH(8) → 抛 IllegalStateException(疑似恶意报文/格式损坏)
 ├─ bytes 为空 → 返回空集合(空嵌套块 / 空 LIST 字段)
 ├─ lineMark = bytes[0]
 │    ├─ 'N' → lineNumber = 1(单行)
 │    └─ 其他 → lineNumber = (bytes[1] << 8) | bytes[2](多行)
 ├─ N = bytes[3];lineNumber == 0 → 返回空列表
 ├─ typeIndex = 4,index = 4 + N,offset = 4 + 3N
 ├─ while (lineNumber > 0)  逐行:
 │    ├─ while (index < 4 + N + 2N)  逐列:
 │    │    ├─ type = getType(bytes[typeIndex])
 │    │    ├─ STRING:长度表恒为 0 → 忽略;改从 offset 读内嵌 2 字节 strLen,
 │    │    │          copy(offset+2, strLen);offset += strLen + 2
 │    │    ├─ LIST:从 offset 读内嵌 2 字节 nestedLen;
 │    │    │        nestedLen == 0 → 值 = 空列表(Collections.emptyList())
 │    │    │        否则切片嵌套帧字节 → 递归 handleLineBytes(nested, depth + 1)
 │    │    │        得到 List<List<MessageMetaData>> 作为该字段值;offset += nestedLen + 2
 │    │    └─ 定长:length = 长度表 2 字节大端;copy(offset, length);offset += length
 │    ├─ 一行结束:结果入 result;typeIndex 复位 4、index 复位 4 + N;lineNumber--
 ├─ 尾部解析名称区 → 每行每列 setName
 └─ 返回 List<List<MessageMetaData>>(行 × 列;LIST 字段值为 List<List<MessageMetaData>>)

5.3 类型还原:getValue(Type, ByteBuf)

按类型标记分别调用 Netty ByteBuf 的读取 API:

类型 还原方式
CHAR getCharSequence(0, 1, UTF_8).charAt(0)
SHORT getShort(0)
FLOAT getFloat(0)
DOUBLE getDouble(0)
INTEGER getInt(0)
LONG getLong(0)
BOOLEAN getBoolean(0)
STRING 读取全部剩余字节转 UTF-8 字符串
其他 null

LIST 不经过 getValue:其值在 handleLineBytes 的 LIST 分支中通过递归解析嵌套帧得到(见 5.2)。

5.4 解码产物:MessageMetaData

MessageMetaData {
    ByteTypeUtil.Type type;   // 字段类型
    Object             data;  // 还原后的值
    String             name;  // 字段名(来自尾部名称区)
}

业务侧按 namedata 并自行强转为对应类型即可使用。当 type == LIST 时,dataList<List<MessageMetaData>>(子表行 × 列),业务侧可继续按 name 递归取用各子表字段。


六、设计要点与边界限制

6.1 长度字段容量

长度表每个字段占 2 字节大端序,容量 0~65535字符串(STRING)长度允许超过 255 字节(容量上限 65535 字节):

  • 单行格式:STRING 长度按实际 UTF-8 字节数写入长度表;
  • 多行格式:STRING/LIST 列在长度表置 0,实际字节长度改为内嵌在值区每行的 2 字节前缀中(见 3.2/3.3),因此多行 STRING 同样支持最长 65535 字节,LIST 嵌套块同样受 2 字节前缀 65535 上限约束。

6.2 多行模式:String 行内变长,定长列天然等长

多行消息只写首行的类型表/长度表,后续行复用。

  • 定长列(INTEGER/LONG/DOUBLE/FLOAT/SHORT/CHAR/BYTE/BOOLEAN):字节宽度由类型唯一决定,天然等长;
  • STRING 列:实际长度内嵌在值区每行的 2 字节前缀中,同一列在不同行允许长度不同(行内变长),解码端按前缀逐个还原,不存在"同列等长"约束。

早期实现曾按首行长度估算总预算导致越界/空洞,2026-08-15 已改为遍历所有行所有列精确累计(见修订记录)。

6.3 单行格式与 lineMark 的兼容性(已统一入口)

单行 1 字节头格式(generateBytes)的 bytes[0] 是字段数 N,若直接交给 handleLineBytes,会被当作 lineMark 判断,当 N == 78('N')N == 89('Y') 时会误判分支导致解析错乱。 当前已统一入口:发送端(InnerTestController)一律使用 generateLineBytes(List.of(map)) 生成 4 字节头消息,服务端 GenerateMessageDataHandler.channelRead 统一走 ByteTypeUtil.handleLineBytes,二者完全对齐。generateBytes 及 1 字节头解析器仅作历史保留。

6.4 名称区尾随换行

split("\\|") 未去除末尾 \n,最后一个字段名会携带换行符("nameN\n")。当前仅用于 setName,若下游用 name 精确匹配需注意。

6.5 消息边界

名称区固定以 \n 结尾,在流式 TCP 场景中可作为下一条消息的切分点(GenerateMessageDataHandler 收到的 byte[] 即按此切分后的单条完整消息)。

6.6 嵌套 LIST 字段(2026-08-23 新增)

LIST 列以"2 字节大端嵌套块长度 + 嵌套帧"递归编码(见 3.3),使单行字段支持多行子结果集List<Map>)与标量列表List<Integer> 等自动包装为单列多行)。要点:

  • 深度上限 8MAX_NEST_DEPTH = 8,序列化/反序列化双侧校验,超限抛 IllegalStateException,防栈溢出与恶意报文攻击。
  • 嵌套帧不带 \n:嵌套块末尾不追加换行符,否则会被帧分隔符 \n 误切成两条消息。
  • 空列表nestedLen = 0,解码端返回空列表。
  • 整帧上限不变:外层仍受 DelimiterBasedFrameDecoder(65535, "\n") 约束,嵌套总数据量锁死在约 64 KB;单值(STRING/LIST)仍有 2 字节长度前缀 65535 上限。

6.7 列数/类型约束:层内一致、跨层自由(2026-08-23 新增)

嵌套块的列数与类型由各层自身的首行决定,与外层互不约束:

  • 同一层内:所有行的列数、各列类型必须与首行一致(fail-fast,抛 IllegalArgumentException),防止静默写坏报文;
  • 跨层:外层 N 列、嵌套层 M 列(M ≠ N)完全合法——“外层 3 列 + 子表 2 列"即可正常往返,支持"外层 20 列 + 子表 20 列"等任意组合。

6.8 Map 迭代顺序

头部类型表/长度表/值区与名称区均基于同一 Map 迭代顺序生成(HashMap 顺序不保证),但解码时列顺序由头部决定、名称仅作标记,故列序不影响正确性(键名与值是一一对应的,只要类型/长度/值三表顺序一致即可)。


七、适合的场景

7.1 高度契合的场景

  1. 高频、低延迟的内网二进制通信

    • 交易/撮合/行情等对时延敏感的场景;ProtocolFastjsonCompareTest 内置与 fastjson 的序列化体积与耗时对照基准(实测见 十、与 fastjson 的基准对比)。
    • 头部紧凑(N 字段单行仅 3N+1 字节头),键名只出现一次,体积远小于 JSON(无引号、冒号、重复键名)。
  2. 同构批量行数据的列式压缩传输

    • 多行模式把类型/长度/名称各存一份,数据区按行列矩阵排布,适合批量订单同步、批量状态上报、批量指标回传等"一张表多条记录"的载荷,压缩比高且解码遍历规则简单。
  3. 环形缓冲(RingBuffer / Disruptor)事件载荷

    • 模块名 native-cloud-common-ringbuffer 表明协议与环形缓冲配套:事件对象携带 byte[] 消息,规避对象池中对象引用竞争,序列化产物不可变,天然适配无锁队列的并发模型。
  4. 长连接 TCP 流式传输

    • InnerTestController 通过 SocketChannel 直接写入 generateLineBytes 产物(4 字节头;单条记录用单元素 List);GenerateMessageDataHandler(Netty ChannelInboundHandlerAdapter)在服务端经 ByteTypeUtil.handleLineBytes 统一解码。\n 边界适合单工/半双工的命令-回执流。
  5. 结构固定、类型简单的参数协议

    • 字段名/类型/数量在收发双方约定固定(如 RPC 命令参数、配置下发、业务标记回写),无需动态 schema 协商。
  6. 嵌套多行结果集(2026-08-23 新增)

    • 单行字段可携带 List<Map> 子表(多级主从 / 树状结果集,如"订单 → 明细 → 商品”),或标量列表;递归长度前缀 + LIST 类型标记,深度上限 8 层。

7.2 不适合的场景

场景 原因
任意对象图 / Map 值 / 枚举 / 泛型 仅支持 10 种基础类型 + 嵌套 LIST(元素须为 Map 或标量),不支持任意 Java 对象图
超长文本(>65535 字节字符串) 长度字段 2 字节容量上限 65535
动态字段结构频繁变更 需收发双端同步类型/长度/名称三表约定
跨语言生态对接 目前绑定 Netty ByteBuf 与 Java 类型体系,需其他语言自行实现编解码

八、总结

Lemon-Protocol 是一套面向高性能 Java 内网通信的紧凑二进制协议:以"字段数 + 类型表 + 长度表 + 值区 + 尾部名称区"为骨架,单行变体面向单条参数、多行变体面向批量行数据。多行 STRING 列通过值区内嵌长度前缀支持行内变长,LIST 列通过递归长度前缀支持嵌套多行结果集(深度上限 8 层),发送/接收已统一为 4 字节头格式,天然适配环形缓冲与长连接流式传输。其优势在于体积小、解码规则简单、零对象复用的并发友好;使用时需注意 String 单值 65535 字节上限、整帧 65535 字节上限、名称区尾随换行等边界条件。


九、测试验证与修订记录

9.1 单元测试:ByteTypeUtilCodecTest(JUnit 5)

测试类位于 native-cloud-common-ringbuffer/src/test/java/com/huang/buffer/ByteTypeUtilCodecTest.java,2026-08-15 新增、2026-08-23 扩展,18 个用例全部通过Tests run: 18, Failures: 0, Errors: 0):

测试方法 场景
testSingleLineAllTypesRoundTrip 单行(单元素 List)8 种类型字段往返
testSingleLineVariableStringLengths 单行同列多 String,长度各异(含 300 字节 UTF-8)
testMultiLineAllTypesRoundTrip 3 行 × 全类型
testMultiLineVariableLengthStringPerRow 核心场景:同一 String 列 3 行长度不同(行内变长)
testMultiLineStringNear65535Bytes 60000 字节 String(2 字节前缀上限内)
testNestedListFieldSingleOuterRow 单外层行 + LIST 子表多行(嵌套往返)
testNestedListFieldMultiOuterRows 多外层行 + LIST 子表(含空子表)
testNestedListEmpty 空列表字段(nestedLen = 0)
testNestedScalarList 标量列表:List<Integer> / List<String> 自动包装单列多行
testNestedListTwoLevels 两层嵌套(外层 → 子表 → 孙表)
testNestedListLongStrings 嵌套块内 600 字节长字符串
testNestedListNearFrameLimit 300 行子表(约 31 KB),接近帧上限
testNestedFiveLevelsTwentyColumnsAllTypes 五层嵌套 × 每层 20 列全类型(根 → L1 → L2 → L3 → L4 → L5,最内层空子表;每层 19 标量 + 1 LIST)
testTypeInconsistencyAcrossRowsThrows 行间列数/类型不一致抛 IllegalArgumentException(fail-fast)
testUnsupportedTypeThrows 不支持类型(如 Map/BigDecimal)抛 IllegalArgumentException
testNestedDepthLimitThrows 9 层嵌套(超过上限 8)抛 IllegalStateException
testEmptyParameters 空列表 / 空 Map 返回空字节数组
testByteTypeDeserializationStatus 记录现状:BYTE 可序列化,但反序列化无 BYTE 分支,data 恒为 null

9.2 2026-08-15 修订记录

问题 修复
定长字段长度区"小端写、大端读"不一致 序列化端 16 处(单行 + 多行)统一改为完整 2 字节大端写,与解码端一致;此前 INTEGER 长度被读出 1024(实际 4)、LONG/DOUBLE 2048、CHAR/BYTE/BOOLEAN 256,导致 copy 越界或数据错乱
长度区游标 +1 错位 单行 lengthIndex = 1 + N + 1、多行 lengthIndex = 4 + N + 1 各删掉多余 +1,与解码端 1+N / 4+N 对齐
多行长度预算只算首行导致越界/空洞 改为遍历所有行所有值精确累计(String 按 2 + 字节数
发送端与接收端格式不统一 InnerTestController 两处发送由单行 1 字节头 generateBytes 改为 4 字节头 generateLineBytes(List.of(map)),与服务端 handleLineBytes 完全对齐

以上缺陷在旧实现下未被端到端验证暴露——新增的 ByteTypeUtilCodecTest 首次对 混合类型 + 行内变长 String 做了全链路往返断言。

9.3 2026-08-23 修订记录

变更 说明
新增 LIST 类型(76 = ‘L’) 类型表扩展为 10 种;list 首字母 l(108) 与 long 冲突,用大写 L(76)
嵌套多行结果集 单行字段支持 List<Map> 子表 / 标量列表;递归长度前缀 + 嵌套帧(末尾不带 \n,防被帧分隔符误切)
递归改造 generateLineBytes / handleLineBytes 增加 depth 参数与 LIST 分支;expandNestedFields 预序列化嵌套块(nestedCache 三层数组)
深度上限 MAX_NEST_DEPTH = 8,序列化/反序列化双侧校验,防栈溢出与恶意报文
schema fail-fast 行间列数/类型不一致、不支持类型(Map/BigDecimal 等)直接抛异常,避免静默写坏报文
标量列表包装 toRows 将标量列表包装为单列多行(列名沿用外层字段名)
测试扩展 7 → 18 个用例,新增 11 个嵌套相关用例(含五层 × 每层 20 列全类型)

十、与 fastjson 的基准对比

10.1 测试方法与环境

  • 基准类ProtocolFastjsonCompareTestnative-cloud-common-ringbuffer/src/test/java/com/huang/buffer/),2026-08-15 新增。
  • 环境:macOS;OpenJDK 17(GraalVM CE 22.3.2);Maven 3.6.3;fastjson 1.2.78(test scope)。
  • 方法:JIT 预热(单数据 5000 次、多数据 200 次)+ 固定迭代(单数据 50000 次、多数据 500 次)取平均单次耗时;volatile sink 消费返回值防止 JIT 死代码消除;两轮采样结果一致(波动 <5%)。
  • 数据集:单数据 = 1 行 × 10 列;多数据 = 1000 行 × 10 列。字段含 INTEGER/LONG/DOUBLE/FLOAT/CHAR/BOOLEAN/STRING×4,字符串含中文。
  • 口径:Lemon = generateLineBytes / handleLineBytes;fastjson = JSON.toJSONString / parseObjectparseArray。字节数 = Lemon 协议字节数组长度 vs JSON 字符串 UTF-8 字节数(网络传输口径)。

10.2 单数据场景(1 行 × 10 列)

指标 Lemon-Protocol fastjson 差异
序列化字节数 181 B 180 B 基本持平(+0.6%)
序列化耗时 2989 ns/次 2592 ns/次 fastjson 快约 15%
反序列化耗时 4919 ns/次 2307 ns/次 fastjson 快约 2.1 倍

10.3 多数据场景(1000 行 × 10 列)

指标 Lemon-Protocol fastjson 差异
序列化字节数 88877 B(约 87 KB) 187071 B(约 183 KB) Lemon 省约 52%
序列化耗时 540182 ns/次(约 540 µs) 832626 ns/次(约 833 µs) Lemon 快约 35%
反序列化耗时 2321653 ns/次(约 2.32 ms) 874340 ns/次(约 874 µs) fastjson 快约 2.7 倍

10.4 结论与原因分析

  1. 字节数:多数据场景 Lemon 优势显著——类型表 + 长度表 + 二进制定长编码去除了 JSON 的键名重复与文本格式开销(键名仅出现一次),体积约为 fastjson 的 47%;单数据场景因头部固定开销(4 + N + 2N + 名称区)占比高,与 JSON 几乎持平(181 vs 180)。
  2. 序列化:多数据场景 Lemon 快约 35%(键名只写一次、定长数值直接写二进制);单数据场景 fastjson 略快约 15%(JIT 对 Map 序列化路径优化成熟,Lemon 单条仍有头部组装开销)。
  3. 反序列化:当前实现 Lemon 明显慢于 fastjson(单数据约 2.1 倍、多数据约 2.7 倍)。主要原因:handleLineBytes 逐行逐列创建 MessageMetaData 对象、经 ByteBuf 读取与 copy 拷贝、逐列走类型分支;批量场景下对象分配与拷贝开销被放大。这是后续优化重点(如按类型表批量解码、列式批量拷贝、减少中间对象)。

说明:耗时与本机 JIT 状态 / JDK 版本强相关;单数据场景差异处于纳秒级,工程上可视为同量级。ProtocolFastjsonCompareTest 保留在测试目录,可随时重跑复现。


附录:协议字节布局示意图

A. 单行消息整体布局(N 字段)

各域定义、偏移与计算规则见 3.1 表格,此处给出字节排布速览(纯 ASCII,等宽字体下对齐):

0           1              1+N               1+3N             1+3N+sumLen
+-----------+---------------+-----------------+----------------+------------------+
| N  (1B)   | type (N x 1B) | len (2N x 2B)   | values        | names "...\n"     |
| 字段数    | 每列1B类型标记  | 每列2B大端长度   | 按列拼接的值    | key1|key2|...|keyN |
+-----------+---------------+-----------------+----------------+------------------+

B. 单行消息逐字节示例(3 字段:id=100(Integer) + name="abc"(String) + flag=true(Boolean)

偏移 hex 值(hex) 说明
0 0x00 03 字段数 N = 3
1 0x01 69 ('i') 第1列类型标记:INTEGER
2 0x02 53 ('S') 第2列类型标记:STRING
3 0x03 42 ('B') 第3列类型标记:BOOLEAN
4 0x04 00 04 第1列值长度 = 4(大端)
6 0x06 00 03 第2列值长度 = 3(大端)
8 0x08 00 01 第3列值长度 = 1(大端)
10 0x0A 00 00 00 64 第1列值:100(大端 int)
14 0x0E 61 62 63 第2列值:“abc”(UTF-8)
17 0x11 01 第3列值:true(1 字节)
18 0x12 69 64 7C 6E 61 6D 65 7C 名称区:“id|name|flag”(续下行)
66 6C 61 67 名称区剩余:flag
30 0x1E 0A 边界符:\n

总长 = 1 + 3 + 6 + 8 + 13 + 1 = 32 字节

C. 多行消息整体布局(M 行 × N 列)

各域定义、偏移与计算规则见 3.2 表格,此处给出字节排布速览(纯 ASCII,等宽字体下对齐):

0           1        3    4              4+N            4+3N             4+3N+sumLen
+-----------+--------+----+--------------+---------------+-----------------+------------------+
| lineMark  | line M | N  | type (N x 1B)| len (2N x 2B) | values          | names "...\n"     |
| 'Y'/'N'   | (2B)   |(1B)| row1 only    | row1 only     | row-major       | row1 keys        |
+-----------+--------+----+--------------+---------------+-----------------+------------------+
                                                          | 定长列按宽平铺;STRING/LIST 每行 2B 前缀 + 值

D. 多行消息逐字节示例(2 行 × 3 列:id(Integer) + price(Double) + code(String="AB"/"CD")

偏移 hex 值(hex) 说明
0 0x00 59 ('Y') lineMark:多行
1 0x01 00 02 行数 M = 2(大端)
3 0x03 03 列数 N = 3
4 0x04 69 ('i') 64 ('d') 53 ('S') 类型区:INTEGER / DOUBLE / STRING
7 0x07 00 04 第1列(id)长度 = 4(大端)
9 0x09 00 08 第2列(price)长度 = 8(大端)
11 0x0B 00 00 第3列(code, STRING)长度表置 0
13 0x0D 00 00 00 64 行1 id = 100(大端 int)
17 0x11 40 59 40 00 …(8B)… 行1 price = 100.5(大端 double)
25 0x19 00 02 41 42 行1 code = “AB”(2B 前缀 00 02 + UTF-8)
29 0x1D 00 00 00 65 行2 id = 101
33 0x21 40 69 20 00 …(8B)… 行2 price = 200.75(大端 double)
41 0x29 00 02 43 44 行2 code = “CD”
45 0x2D 69 64 7C 70 72 69 63 65 7C 名称区:“id|price|code”(续下行)
63 6F 64 65 名称区剩余:code
58 0x3A 0A 边界符:\n

总长 = 4(头) + 3(类型) + 6(长度) + 32(值区) + 14(名称区) = 59 字节 值区 = 行1(4+8+2+2) + 行2(4+8+2+2) = 32 字节(code 每行含 2 字节长度前缀)

E. 解码指针轨迹(多行为例,handleLineBytes

lineMark→(判单/多行) → lineNumber(M) → N → 指针初始化:
   typeIndex = 4(类型区起点)   index = 4+N(长度区起点)   offset = 4+3N(值区起点)
   每列:
     STRING:长度表置 0 → 忽略;改读 offset 处内嵌 2B strLen → copy(offset+2, strLen) → offset += strLen+2
     LIST:读 offset 处内嵌 2B nestedLen → 切片嵌套帧 → 递归 handleLineBytes(nested, depth+1) → offset += nestedLen+2
     定长:读长度表 2B length → copy(offset, length) → offset += length
   每行结束:typeIndex 复位 4、index 复位 4+N、offset 连续、M--
 名称区:从当前 offset 到末尾 split("\\|") 得键名,按列 set 到每行的 MessageMetaData

F. LIST 嵌套字段字节示例(外层 1 行 × 2 列:id=1(Integer) + orders=LIST

外层行 2 列,其中 orders 为嵌套子表(2 行 × 1 列 STRING oid="101"/"102")。注意外层 2 列、嵌套层 1 列——印证"层内一致、跨层自由"(见 6.7)。

外层帧:

偏移 hex 值(hex) 说明
0 0x00 4E ('N') lineMark:单行(统一走 4 字节头)
1 0x01 00 01 行数 = 1(单行时忽略)
3 0x03 02 列数 N = 2
4 0x04 69 ('i') 4C ('L') 类型区:INTEGER / LIST
6 0x06 00 04 第1列(id)长度 = 4
8 0x08 00 00 第2列(orders, LIST)长度表置 0
10 0x0A 00 00 00 01 第1列值:id = 1
14 0x0E 00 17 第2列值:嵌套块长度 nestedLen = 23
16 0x10 59 00 02 01 53 00 00 00 03 31 30 31 00 03 31 30 32 6F 72 64 65 72 73 嵌套帧(23 字节,见下)
39 0x27 69 64 7C 6F 72 64 65 72 73 名称区:“id|orders”(无 \n
48 0x30 0A 边界符:\n

外层总长 = 1(头) + 2(行数) + 1(列数) + 2(类型) + 4(长度) + 4(id) + 25(orders = 2B 前缀 + 23B 嵌套帧) + 9(名称区) + 1(\n) = 49 字节

嵌套帧(orders 子表,nestedLen = 23,相对嵌套帧偏移):

偏移 hex 值(hex) 说明
0 0x00 59 ('Y') lineMark:多行
1 0x01 00 02 行数 = 2
3 0x03 01 列数 = 1(与外层 2 列不同,合法)
4 0x04 53 ('S') 类型区:STRING
5 0x05 00 00 长度表置 0
7 0x07 00 03 31 30 31 行1 oid = “101”
12 0x0C 00 03 31 30 32 行2 oid = “102”
17 0x11 6F 72 64 65 72 73 名称区:“orders”(appendTerminator = false,无 \n

嵌套帧总长 = 4(头) + 1(类型) + 2(长度) + 5 + 5(两行值,各含 2B 前缀) + 6(名称区) = 23 字节