如何使用C#解析KLV格式字节数组并构造KLV发送数据
C# 实现UART通信场景下KLV格式数据的解析与构造
KLV是嵌入式串行通信中非常通用的载荷格式,核心结构为键(Key)-长度(Length)-值(Value) 三元组,不同协议的差异仅在于Key的字节长度、Length的编码规则(定长/可变长BER)、字节序(大端/小端),以下实现默认采用工业场景最常用的配置:1字节Key、2字节大端无符号数表示Length,你可以对照自己的设备协议调整参数即可。
接收端:字节数组解析KLV
解析核心流程是顺序遍历字节流,按三元组结构逐段读取,核心要处理UART流式传输带来的半包、粘包问题:
- 先跳过帧前导码、无效填充字节,定位到第一个有效KLV条目的起始偏移
- 按协议定义长度读取Key,映射对应的业务字段类型
- 按Length编码规则读取Value的字节长度
- 按长度读取对应长度的Value字节,根据Key对应的类型(整数、浮点数、字符串、结构体)转成业务值
- 偏移读取指针到下一个KLV条目起始位置,重复上述流程直到缓冲区剩余字节不足一个最小KLV单元,剩余字节缓存后和下次接收数据拼接再解析
解析代码示例
// 存储解析后的KLV条目 public struct KlvItem { public byte Key; public byte[] Value; } /// <summary> /// 解析KLV格式原始字节数组 /// </summary> /// <param name="rawData">UART接收到的字节数组</param> /// <param name="keyLen">Key字段字节长度,默认1</param> /// <param name="lenFieldLen">Length字段字节长度,默认2</param> /// <param name="isLittleEndian">Length/多字节Value是否为小端序,默认大端</param> /// <returns>解析完成的KLV条目列表</returns> public List<KlvItem> ParseKlv(byte[] rawData, int keyLen = 1, int lenFieldLen = 2, bool isLittleEndian = false) { var result = new List<KlvItem>(); int offset = 0; while (offset < rawData.Length) { // 剩余字节不足最小KLV单元,判定为半包,直接退出留待下次拼接解析 if (offset + keyLen + lenFieldLen > rawData.Length) break; // 读取Key byte key = rawData[offset]; offset += keyLen; // 读取Value长度 byte[] lenBytes = new byte[lenFieldLen]; Buffer.BlockCopy(rawData, offset, lenBytes, 0, lenFieldLen); offset += lenFieldLen; if (isLittleEndian) Array.Reverse(lenBytes); int valueLen = BitConverter.ToUInt16(lenBytes, 0); // 剩余字节不足Value长度,判定为半包 if (offset + valueLen > rawData.Length) break; // 读取Value byte[] value = new byte[valueLen]; Buffer.BlockCopy(rawData, offset, value, 0, valueLen); offset += valueLen; result.Add(new KlvItem { Key = key, Value = value }); } return result; }
Value转业务值示例
解析得到的Value是原始字节数组,根据Key对应的字段类型转换即可:
List<KlvItem> klvItems = ParseKlv(uartRecvBuffer); foreach (var item in klvItems) { switch (item.Key) { case 0x01: // Key=0x01 对应2字节无符号数,代表设备电压,单位mV if (item.Value.Length == 2) { ushort voltage = BitConverter.ToUInt16(item.Value, 0); // 业务逻辑处理 } break; case 0x02: // Key=0x02 对应ASCII编码字符串,代表设备序列号 string serial = Encoding.ASCII.GetString(item.Value).TrimEnd('\0'); break; // 其余Key按协议定义依次处理 } }
发送端:构造KLV格式发送帧
构造流程和解析完全反向,逐个拼接三元组后,再按协议要求添加帧头、校验、帧尾即可:
/// <summary> /// 构造单个KLV条目的字节数组 /// </summary> public byte[] BuildSingleKlv(byte key, byte[] value, int keyLen = 1, int lenFieldLen = 2, bool isLittleEndian = false) { List<byte> itemBuffer = new List<byte>(); // 写入Key itemBuffer.Add(key); // 写入Value长度 ushort valueLen = (ushort)value.Length; byte[] lenBytes = BitConverter.GetBytes(valueLen); if (!isLittleEndian) Array.Reverse(lenBytes); itemBuffer.AddRange(lenBytes); // 写入Value itemBuffer.AddRange(value); return itemBuffer.ToArray(); } // 构造完整发送帧示例 List<byte> sendFrame = new List<byte>(); // 写入协议规定的帧头 0xAA 0x55 sendFrame.Add(0xAA); sendFrame.Add(0x55); // 添加KLV载荷1:Key=0x10 代表设置LED亮度,Value为0x64(100%亮度) sendFrame.AddRange(BuildSingleKlv(0x10, new byte[] { 0x64 })); // 添加KLV载荷2:Key=0x11 代表设置上报间隔,Value为1000ms(2字节大端无符号数) byte[] intervalVal = BitConverter.GetBytes((ushort)1000); Array.Reverse(intervalVal); sendFrame.AddRange(BuildSingleKlv(0x11, intervalVal)); // 写入异或校验位 byte checksum = 0; foreach (byte b in sendFrame) checksum ^= b; sendFrame.Add(checksum); // 写入帧尾 0x0D sendFrame.Add(0x0D); // sendFrame.ToArray()即可直接通过UART串口发送
踩坑提示
- 调不通优先核对字节序、字段长度,90%的KLV通信异常都是大小端不匹配、Key/Length字段长度和设备端定义不一致导致的
- 不要忽略半包处理,UART是流式传输,没有天然包边界,一次Receive拿到的可能是半个包也可能是多个包拼在一起
- 如果协议采用可变长BER编码的Length字段(常见于MISB地理信息、智能卡类协议),只需要替换固定读取Length的逻辑为BER解码逻辑,整体流程不变
内容的提问来源于stack exchange,提问作者user1610609
相关产品推荐
相关产品推荐

