TLV 编码解析
问题背景
TLV(Tag-Length-Value) 是一种常见且高效的数据编码方案。一个 TLV 数据单元由三个部分组成:
- T (Tag - 标签) : 标识数据的类型。
- L (Length - 长度) : 定义数据内容的长度。
- V (Value - 内容) : 实际的数据。
本问题要求解析一个以十六进制字符串形式表示的 TLV 码流。
码流格式与解析规则
给定的码流是一个连续的十六进制字符串,其中每两位字符代表一个字节。解析遵循以下规则:
-
基本结构:
- T 域: 码流的前 2 个字节(4 个十六进制字符)。
- L 域: 紧随 T 域的 4 个字节(8 个十六进制字符)。
- V 域: 紧随 L 域,其字节数由 L 域的值(设为
len)决定。
-
字节序 (Endianness) :
- 所有多字节字段(如 T 域和 L 域)均采用大端序(Big-Endian) 。这意味着高位字节存储在内存的低地址。例如,4 字节的长度
0000000a代表十进制的 10。
- 所有多字节字段(如 T 域和 L 域)均采用大端序(Big-Endian) 。这意味着高位字节存储在内存的低地址。例如,4 字节的长度
-
节点类型 (由 T 域决定) :
-
通过检查 T 域的**最高有效位(Most Significant Bit, MSB)**来判断 V 域的内容类型:
-
MSB = 0: 该 TLV 是一个非嵌套(或称原始)节点。
- V 域的内容是字符串。
- L 域的值
len表示该字符串的字节长度。 - 这是我们需要提取并输出的目标内容。
-
MSB = 1: 该 TLV 是一个嵌套(或称构造)节点。
- V 域本身包含 一个或多个 完整的、连续的子 TLV 结构。
- L 域的值
len表示其 V 域内所有子 TLV 的总字节长度。
-
-
任务要求
给定一个十六进制 TLV 码流 hexStream,请递归解析该码流,并按解析顺序输出所有非嵌套 TLV 节点的内容。
输入格式
-
hexStream: 一个字符串,代表完整的 TLV 码流。- 由字符集
[0-9a-f]组成。 - 长度为偶数,且
12 <= hexStream.length <= 20000。 - 输入保证是一个合法的、完整的 TLV 结构。
- 对于非嵌套节点,其 V 域长度
len的范围是[0, 256],且内容均为可见的 ASCII 字符。
- 由字符集
输出格式
-
一个字符串列表,其中每个字符串代表一个解析出的非嵌套 TLV 节点,格式为:
"level:content"。level: 该 TLV 所在的嵌套级别(十进制整数)。根节点的级别为0,其直接子节点的级别为1,以此类推。取值范围[0, 20]。content: 最终解析出的 V 域字符串。如果 V 域长度为0,则 content 为空字符串(例如"1:")。
提示信息
-
可见字符: ASCII 码值从 32 到 126 的字符,包括空格、数字、英文字母和标点符号。
-
大端序 vs. 小端序: 计算机存储多字节数据的方式。例如,整数
65538的十六进制是0x00010002。-
大端序存储为:
00 01 00 02 -
小端序存储为: 02 00 01 00
本题使用大端序。
-
样例说明
样例 1
-
输入:
"00000000000a68656c6c6f20776f7264" -
输出:
["0:hello word"] -
解释:
-
解析级别 0:
- T 域:
0000。最高位是0,表示这是一个非嵌套节点。 - L 域:
0000000a。大端序,转换为十进制为10。这表示 V 域有 10 个字节。 - V 域: 读取接下来的 10 个字节(20 个十六进制字符)
68656c6c6f20776f7264。 - 解码 V 域: 将十六进制
68转为字符h,65转为e...20转为空格 ... 最终得到字符串"hello word"。
- T 域:
-
输出结果: 级别为
0,内容为"hello word",格式化为"0:hello word"。
-
样例 2
-
输入:
"c0000000003f00000000000a68656c6c6f20776f7264c0000000001000000000000a68656c6c6f20776f726400000000000a68656c6c6f20776f7264000000000003545454" -
输出:
["1:hello word", "2:hello word", "1:hello word", "1:TTT"] -
解释:
-
解析级别 0 (根节点) :
- T 域:
c000(二进制1100...)。最高位是1,表示这是一个嵌套节点。 - L 域:
0000003f,十进制为63。V 域包含63字节的子节点数据。 - V 域: 开始递归解析这
63字节的内容,当前嵌套级别进入1。
- T 域:
-
解析级别 1 (第一个子节点) :
- T 域:
0000。最高位0-> 非嵌套节点。 - L 域:
0000000a(10)。 - V 域: 解码为
"hello word"。 - 记录输出:
1:hello word。 - 此节点总长为
T(2) + L(4) + V(10) = 16字节。从 V 域的63字节中消耗16字节。
- T 域:
-
解析级别 1 (第二个子节点) :
-
T 域:
c000。最高位1-> 嵌套节点。 -
L 域:
00000010(16)。其 V 域包含16字节的子节点。 -
进入 级别 2 进行解析。
- 解析级别 2: T=
0000(非嵌套), L=0000000a(10), V="hello word"。 - 记录输出:
2:hello word。
- 解析级别 2: T=
-
此级别1节点总长为
T(2) + L(4) + V(16) = 22字节。从 V 域的63字节中再消耗22字节。
-
-
解析级别 1 (第三个子节点) :
- T 域:
0000。最高位0-> 非嵌套节点。 - L 域:
0000000a(10)。 - V 域: 解码为
"hello word"。 - 记录输出:
1:hello word。 - 此节点总长为
16字节。
- T 域:
-
解析级别 1 (第四个子节点) :
- T 域:
0000。最高位0-> 非嵌套节点。 - L 域:
00000003(3)。 - V 域:
545454解码为"TTT"。 - 记录输出:
1:TTT。 - 此节点总长为
T(2) + L(4) + V(3) = 9字节。
- T 域:
-
结束: 级别 0 的 V 域
63字节已全部解析完毕 (16 + 22 + 16 + 9 = 63)。按发现顺序组合最终输出。
-
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 解决多级嵌套TLV解析问题的实现类.
* 核心思想是使用递归来处理TLV的嵌套结构。
*/
public class TlvParser {
/**
* 主方法,解析一个十六进制表示的TLV码流.
* @param hexStream 待解析的十六进制字符串
* @return 按顺序包含所有非嵌套TLV内容的字符串列表
*/
public List<String> parse(String hexStream) {
// 将输入的十六进制字符串转换为字节数组
byte[] data = hexStringToByteArray(hexStream);
// 用于存储最终结果的列表
List<String> results = new ArrayList<>();
// 从0级别开始,对整个字节流进行解析
parseTlvStream(data, 0, data.length, 0, results);
return results;
}
/**
* 递归辅助方法,用于解析一个包含一个或多个TLV结构的字节流片段.
* @param data 完整的字节数组
* @param offset 当前要开始解析的偏移量
* @param length 需要解析的总长度 (即当前V域的长度)
* @param level 当前的嵌套级别
* @param results 用于收集结果的列表
*/
private void parseTlvStream(byte[] data, int offset, int length, int level, List<String> results) {
int currentPos = offset;
// 循环处理,直到当前V域的所有字节都被解析完毕
while (currentPos < offset + length) {
// 解析当前位置的一个TLV单元,并获取下一个单元的起始位置
currentPos = parseOneTlv(data, currentPos, level, results);
}
}
/**
* 解析单个TLV单元,并根据其类型决定是递归深入还是提取内容.
* @return 解析完这个TLV单元后,下一个TLV单元的起始偏移量
*/
private int parseOneTlv(byte[] data, int offset, int level, List<String> results) {
// 读取T域 (Tag),2个字节
int tag = bytesToInt(data, offset, 2);
// 读取L域 (Length),4个字节
int len = bytesToInt(data, offset + 2, 4);
// V域的起始位置
int valueOffset = offset + 6; // 2字节Tag + 4字节Length
// 检查Tag的最高位 (对于2字节的short,最高位是第15位)
// 0x8000 的二进制是 10000000 00000000
boolean isNested = (tag & 0x8000) != 0;
if (isNested) {
// 如果是嵌套类型,则对其V域进行递归解析,嵌套级别+1
parseTlvStream(data, valueOffset, len, level + 1, results);
} else {
// 如果是非嵌套类型(原始数据),则提取其V域内容
String content = bytesToString(data, valueOffset, len);
// 按照 "level:content" 的格式添加到结果列表中
results.add(level + ":" + content);
}
// 返回下一个TLV单元的起始位置
return valueOffset + len;
}
// --- 辅助工具方法 ---
/**
* 将十六进制字符串转换为字节数组.
*/
private byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
// 1. 获取高位和低位字符
// - s.charAt(i) 是成对字符中的第一个(高位),例如 "1F" 中的 '1'
// - s.charAt(i + 1) 是成对字符中的第二个(低位),例如 "1F" 中的 'F'
// 2. 将字符转换为其代表的数值
// - Character.digit(char c, int radix) 是一个非常有用的方法。
// - Character.digit(s.charAt(i), 16) 会将 '1' 转换为整数 1。
// - Character.digit(s.charAt(i + 1), 16) 会将 'F' 转换为整数 15。
// 3. 位运算:移动高位并合并
// - (Character.digit(s.charAt(i), 16) << 4)
// - << 4 是一个“按位左移”操作,将一个数的二进制表示向左移动 4 位。
// - 假设我们处理的是 '1',其值为 1 (二进制 00000001)。
// - 左移 4 位后变成 00010000 (十进制的 16)。
// - 这个操作的目的是将高位半字节的值移动到字节的高4位位置上。
// 4. 加法合并
// - (高位的值) + (低位的值)
// - 继续我们的例子:(1 << 4) + 15 = 16 + 15 = 31。
// - 在二进制层面看就是:00010000 + 00001111 = 00011111。
// - 这样,两个4位的半字节就成功合并成了一个8位的字节。
// 5. 类型转换
// - (byte) ...
// - 上一步计算的结果是 int 类型,需要强制转换为 byte 类型才能存入字节数组。
// 6. 存入数组
// - data[i / 2] = ...
// - 当 i=0 时,结果存入 data[0]。
// - 当 i=2 时,结果存入 data[1]。
// - ... 以此类推,位置正确。
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* 从字节数组的指定位置读取指定长度的字节,并将其转换为一个整数(大端序).
*/
private int bytesToInt(byte[] bytes, int offset, int len) {
// 因为题目要求最多读取4个字节,所以value为int就行
int value = 0;
for (int i = 0; i < len; i++) {
// 将当前值左移8位,为下一个字节腾出空间
value <<= 8;
// 使用位或操作将下一个字节的值合并进来
// (bytes[...] & 0xFF) 是为了确保将有符号的byte作为无符号数处理
// 无论 bytes[i] 原本是正数还是负数,这个操作都会将它转换成一个 int,
// 并且只有最低的8位保留了原字节的值,所有高位都变成了 0。
// 例如,byte b = -1; (0xFF)。(b & 0xFF)的结果是 int 类型的 255 (0x000000FF),而不是 -1 (0xFFFFFFFF)。
value |= (bytes[offset + i] & 0xFF);
}
return value;
}
/**
* 从字节数组的指定位置读取指定长度的字节,并将其转换为一个字符串 (ASCII编码).
*/
private String bytesToString(byte[] bytes, int offset, int len) {
if (len == 0) {
return "";
}
return new String(bytes, offset, len, StandardCharsets.US_ASCII);
}
}