4、协议解析入门:二进制数据与字节序、struct模块的使用、解析固定长度消息头
好,咱们进入实战环节了。
前面几章我们聊了行情数据的整体架构,也看了网络传输的底层逻辑。但有个问题一直悬着——数据到了我们手里,怎么把它读懂?
说白了,交易所发过来的就是一串二进制流。0和1的排列组合。你看着是乱码,但人家有严格的规矩。这个规矩,就是协议。
我刚开始做量化那会儿,第一次拿到深交所的行情快照,打开一看全是乱码。当时我还以为是网络传输出了问题,折腾了半天。后来才明白,是我自己没按规矩来解析。
嗯,今天我们就来搞定这件事。
4.1 二进制数据长什么样?
先看个最简单的例子。假设我们要传输一个整数 12345。
用文本协议,它会被编码成字符串 "12345",占5个字节。用二进制协议,它就是一个16位的整数,占2个字节。
哪个效率高?显然是二进制。这也是为什么所有交易所的行情协议都选择二进制格式——快,省带宽。
但二进制有个麻烦:字节序。
字节序(Endianness) 指的是多字节数据在内存中的排列顺序。
- 大端序(Big-Endian):高位字节在前,低位字节在后。像我们写数字一样自然。
- 小端序(Little-Endian):低位字节在前,高位字节在后。Intel x86 架构的默认方式。
举个例子,整数 0x12345678:
| 字节序 | 内存排列(从低地址到高地址) |
|---|---|
| 大端序 | 12 34 56 78 |
| 小端序 | 78 56 34 12 |
我在项目中遇到过最坑的一次:从某家期货公司拿到的行情数据,文档写的是大端序,结果实际发过来是小端序。解析出来的价格全是天文数字。排查了整整一个下午,最后发现是文档写错了。
避坑指南:拿到任何协议文档,第一件事就是确认字节序。不要相信文档,要拿实际数据验证。
4.2 Python 的 struct 模块
Python 里处理二进制数据,最趁手的工具就是 struct 模块。它能把 Python 的数据类型和 C 语言的结构体互相转换。
核心就两个函数:
pack(fmt, v1, v2, ...):把 Python 值打包成二进制字节串unpack(fmt, buffer):从二进制字节串中解包出 Python 值
其中 fmt 是格式字符串,它决定了怎么打包/解包。
来看个例子:
import struct
# 打包一个整数和一个浮点数
data = struct.pack('>if', 12345, 3.14)
print(data) # b'\x00\x00\x30\x39@H\xf5\xc3'
# 解包回来
val1, val2 = struct.unpack('>if', data)
print(val1, val2) # 12345 3.140000104904175
注意那个 '>' 符号,它表示大端序。如果是小端序,用 '<'。
常用格式字符:
| 字符 | C 类型 | Python 类型 | 字节数 |
|---|---|---|---|
c | char | bytes (长度1) | 1 |
h | short | int | 2 |
i | int | int | 4 |
q | long long | int | 8 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | bytes | 指定长度 |
4.3 解析固定长度消息头
几乎所有行情协议,都会有一个固定长度的消息头。消息头里通常包含:
- 消息类型(2字节或4字节)
- 消息体长度(2字节或4字节)
- 序列号(4字节或8字节)
- 时间戳(4字节或8字节)
我以某交易所的行情协议为例,它的消息头结构如下:
| 字段 | 类型 | 字节数 | 说明 |
|---|---|---|---|
| MsgType | uint16 | 2 | 消息类型,大端序 |
| BodyLength | uint16 | 2 | 消息体长度,大端序 |
| SeqNo | uint32 | 4 | 序列号,大端序 |
| SendTime | uint64 | 8 | 发送时间(纳秒),大端序 |
消息头一共16个字节。解析代码很简单:
import struct
HEADER_FORMAT = '>HHIQ' # 2+2+4+8 = 16 字节
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
def parse_header(data):
"""解析固定长度消息头"""
if len(data) < HEADER_SIZE:
raise ValueError(f'数据长度不足,需要{HEADER_SIZE}字节,实际{len(data)}字节')
msg_type, body_length, seq_no, send_time = struct.unpack(HEADER_FORMAT, data[:HEADER_SIZE])
return {
'msg_type': msg_type,
'body_length': body_length,
'seq_no': seq_no,
'send_time': send_time,
'header_size': HEADER_SIZE
}
你看,就这么几行代码。但实际项目中,我会加一些校验逻辑:
def parse_header_safe(data):
"""带校验的消息头解析"""
if len(data) < HEADER_SIZE:
return None, f'数据长度不足,需要{HEADER_SIZE}字节'
try:
header = parse_header(data)
except struct.error as e:
return None, f'struct解包失败: {e}'
# 校验消息体长度是否合理
if header['body_length'] > 1024 * 1024: # 超过1MB,明显不合理
return None, f'消息体长度异常: {header["body_length"]}'
# 校验序列号是否连续(可选)
# 这里可以维护一个全局的 last_seq_no 做对比
return header, None
我曾经在生产环境遇到过一个问题:某次行情数据被截断了,消息头里的 body_length 指向了一个巨大的值。结果程序直接申请了 2GB 内存,然后 OOM 崩溃了。从那以后,我所有解析代码里都加了长度校验。
4.4 完整解析流程
有了消息头,解析完整消息就很简单了:
def parse_message(data):
"""解析完整消息"""
header, err = parse_header_safe(data)
if err:
return None, err
total_size = header['header_size'] + header['body_length']
if len(data) < total_size:
return None, f'数据不完整,需要{total_size}字节,实际{len(data)}字节'
body = data[header['header_size']:total_size]
# 根据消息类型,解析不同的消息体
if header['msg_type'] == 1:
return parse_market_data(body, header)
elif header['msg_type'] == 2:
return parse_order_book(body, header)
else:
return None, f'未知消息类型: {header["msg_type"]}'
这个模式很通用。你想想看,不管后面消息体多复杂,只要消息头固定,解析流程都是一样的:
- 先读固定长度的消息头
- 从消息头里拿到消息体长度
- 再读消息体
- 根据消息类型做不同的处理
4.5 知识体系图
下面这张图,把本章的核心逻辑串起来了:
这张图其实就说了三件事:
- 拿到数据,先别急着解包,搞清楚字节序
- 用 struct,按格式字符串解包消息头
- 拿到消息头,就知道后面怎么处理了
我个人习惯,在写任何行情解析代码之前,都会先画这么一张图。把流程理清楚了,代码写起来就顺了。
小技巧:调试二进制数据时,用 hexdump 或者 Python 的 binascii.hexlify 把数据打印成十六进制,肉眼比对协议文档,能快速定位问题。
好了,二进制解析的入门就到这里。下一节我们会深入消息体的解析,看看那些复杂的行情数据结构怎么处理。
公众号:蓝海资料掘金营,微信deep3321