Python struct 解析二进制:先固定字节序,再校验报文长度

10-01 3阅读

同一组二进制数据在自己电脑上能够往返解析,并不能证明别的程序也会读出相同结果。二进制协议需要先约定字段顺序、宽度、符号和字节序。struct 的默认模式会采用本机布局,可能包含对齐填充,因此不适合靠“本地试过”来定义跨机器交换格式。

本文构造一个很小的协议头:一字节版本号、两字节类型号、四字节序号,全部为无符号整数,按网络字节序排列。三个字段合计七字节。这里只讨论完整协议头的编码与验收,读网络流时如何收集完整消息,属于另外一层工作。

Python struct 解析二进制:先固定字节序,再校验报文长度

AI概念配图,非真实界面:以抽象物件说明本文主题,不代表运行结果。

让格式串成为清楚的协议约定

下面程序保存为 demo.py 后用 Python 3.8 或更高版本运行。格式串开头的感叹号选择网络字节序,也就是大端,并使用标准字段尺寸且不插入本机对齐填充。Struct 对象的 size 直接给出所需字节数,后续验收引用它,避免手写一个后来忘记修改的长度常量。

类型号选为十六进制一二三四,序号选为零一零二零三零四,这样打印出的字节能一眼看清先后顺序。若只用零或连续重复字节当样本,即使两端字节序写反,也可能因为测试数据过于对称而没有暴露问题。

import struct

HEADER = struct.Struct("!BHI")
assert HEADER.size == 7

def decode_header(data):
    if len(data) != HEADER.size:
        raise ValueError("header length mismatch")
    version, kind, sequence = HEADER.unpack(data)
    if version != 1:
        raise ValueError("unsupported version")
    return version, kind, sequence

packet = HEADER.pack(1, 0x1234, 0x01020304)
assert packet == bytes.fromhex("01 12 34 01 02 03 04")
assert decode_header(packet) == (1, 0x1234, 0x01020304)
print(packet.hex(" "))

for bad in [packet[:-1], packet + b"x", HEADER.pack(2, 0, 0)]:
    try:
        decode_header(bad)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid header accepted")

assert struct.unpack("!H", b"\x12\x34")[0] == 0x1234
assert struct.unpack("<H", b"\x12\x34")[0] == 0x3412
assert HEADER.unpack_from(packet + b"tail") == (1, 0x1234, 0x01020304)
try:
    HEADER.pack(256, 0, 0)
except struct.error:
    pass
else:
    raise AssertionError("overflow accepted")
print("byte order, exact length and range checks passed")

能读到字段,不等于整份输入有效

程序第一行应输出 01 12 34 01 02 03 04,随后所有边界断言通过。decode_header 要求输入长度恰好等于协议头长度,因此少一个字节和多一个字节都会被拒绝。这里严格拒绝尾部是示例的接口约定,而不是所有二进制协议都必须采用的规则。

unpack 要求缓冲区大小与格式匹配;unpack_from 可以从较大的缓冲区指定位置读取,只要求那里有足够数据。后者适合解析大消息中的一个片段,但成功返回不会替你证明剩余字节合法。若换用它,调用者必须清楚记录读取偏移与剩余长度。

同样的两个字节,用大端和小端读取会得到不同整数。改变字节序并不改变传入缓冲区,只是改变解释方式。排查设备数据时,应对照协议或对端代码确认约定,不能以哪个结果“看起来比较合理”作为唯一判断依据。

字段范围之外还有业务范围

一字节无符号字段只能表示零到二百五十五。示例用二百五十六触发 struct.error,验证编码阶段不会静默截断。实际入口可以提前给出更友好的错误信息,但不要先对数值取模再打包,那会把无效输入变成另一份看似合法的消息。

即使字节宽度和长度都正确,版本号仍可能不被支持,类型号也可能没有定义。代码因此单独检查版本号;完整实现还应根据协议检查保留位、可用类型和字段之间的关系。结构解析负责恢复数值,业务校验负责判断这些数值是否可以接受。

如果协议需要文本字段,还应明确编码和长度单位。字符数与编码后的字节数可能不同,固定长度字符串格式也有补零或截断行为。不要把文本直接塞进整数示例里继续复用假设,先为该字段补齐独立的正反样本。

把这七个固定字节保留成跨语言测试样本很有价值。让发送端和接收端分别验证同一组十六进制内容,再补充边界值、错误版本和长度异常,就能把“双方恰好在同一台机器运行”升级为可重复检查的协议兼容性。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。