3、常见数据类型:整数类型(uint8/16/32/64)、浮点数类型、字符串与变长字段、位域与标志位
好,咱们直接切入正题。做二进制协议解析,说白了就是跟各种数据类型打交道。你想想看,交易所服务器发过来的数据,本质上就是一串字节流。怎么把这串字节流还原成有意义的行情数据、订单状态?关键就在于搞懂这些基础数据类型。
我个人习惯,把二进制协议里的数据类型分成四大类:整数、浮点数、字符串、位域。今天咱们一个一个过,每个我都会结合实战经验来讲。
3.1 整数类型:uint8/16/32/64
整数是二进制协议里最常用的类型。没有之一。交易所的订单号、价格、数量、时间戳,绝大多数都是整数。
先看一张图,帮你快速建立直观印象:
这张图很直观:uint8占1字节,uint16占2字节,uint32占4字节,uint64占8字节。范围分别是0-255、0-65535、0-4294967295、0-18446744073709551615。
核心要点:解析整数时,必须注意字节序(Endianness)。交易所协议通常使用网络字节序(Big-Endian),但也不排除某些小众交易所用Little-Endian。我建议你写代码时,永远显式指定字节序,不要依赖默认值。
3.2 浮点数类型
浮点数在交易所协议里用得不多,但一旦遇到,就容易踩坑。主要用在一些特殊的价格字段或者统计指标上。
二进制协议里的浮点数,99%的情况是IEEE 754标准的单精度(float,4字节)或双精度(double,8字节)。
// C语言示例:解析浮点数
#include <stdint.h>
// 从字节流解析float
float parse_float(const uint8_t* data) {
float value;
// 注意:这里假设已经是本机字节序
// 如果是从网络接收,需要先做字节序转换
memcpy(&value, data, sizeof(float));
return value;
}
// 从字节流解析double
double parse_double(const uint8_t* data) {
double value;
memcpy(&value, data, sizeof(double));
return value;
}
避坑指南:我曾经在解析一个海外交易所的深度数据时,发现价格总是对不上。查了两天,最后发现对方用的是定点数(fixed-point),而不是浮点数。他们把价格乘以10000后存成uint32。所以,拿到协议文档后,先确认清楚:到底是真浮点,还是伪浮点?
3.3 字符串与变长字段
字符串处理是二进制协议解析里最灵活、也最容易出bug的地方。交易所的订单ID、交易对名称、错误信息,很多都是字符串。
常见的字符串编码方式有三种:
| 编码方式 | 说明 | 示例 |
|---|---|---|
| 固定长度 | 字段长度固定,不足补'\0'或空格 | char symbol[8] |
| 长度前缀 | 先存长度值,再存字符串内容 | uint8 len + char data[len] |
| 空字符结尾 | 类似C语言的字符串,以'\0'结束 | "BTCUSDT\0" |
我个人最推荐长度前缀方式。为什么?因为安全。固定长度容易浪费空间,空字符结尾容易受攻击(如果对方没发'\0',你的程序可能一直读到内存越界)。
// Python示例:解析长度前缀字符串
def parse_length_prefixed_string(data, offset):
"""
解析长度前缀字符串
返回:(字符串值, 新偏移量)
"""
# 先读长度(假设是uint16)
length = int.from_bytes(data[offset:offset+2], 'big')
offset += 2
# 再读字符串内容
string_bytes = data[offset:offset+length]
offset += length
# 解码成字符串(通常用UTF-8)
return string_bytes.decode('utf-8'), offset
小技巧:解析变长字段时,一定要做边界检查。我曾经见过一个协议,长度字段写的是65535,但实际数据包只有100字节。如果不检查,直接memcpy就会崩溃。所以,我的代码里永远会加一句:if (length > remaining_bytes) { 报错 }。
3.4 位域与标志位
位域是二进制协议里最精妙的设计。它用几个bit来表示开关状态,能极大节省带宽。交易所的订单状态、交易类型、错误码,经常用位域来表示。
举个例子,一个字节的位域可以表示8个独立的标志位:
你看,一个字节就能表示8种不同的状态。在带宽宝贵的场景下,这种设计非常高效。
// C语言示例:解析位域
// 假设一个字节的位域定义如下:
// bit7-bit6: 订单类型 (00=限价, 01=市价, 10=止损, 11=保留)
// bit5: 买卖方向 (0=买, 1=卖)
// bit4-bit0: 状态标志
uint8_t flags = data[offset];
// 提取订单类型
uint8_t order_type = (flags >> 6) & 0x03;
// 提取买卖方向
uint8_t side = (flags >> 5) & 0x01;
// 提取状态标志
uint8_t status = flags & 0x1F;
// 用switch处理订单类型
switch(order_type) {
case 0: // 限价单
break;
case 1: // 市价单
break;
case 2: // 止损单
break;
default: // 保留或未知
break;
}
关键提醒:解析位域时,一定要用位运算(&、|、>>、<<),不要用结构体对齐。不同编译器对位域的内存布局处理不一样,用结构体位域写出来的代码,换个编译器可能就解析错了。我吃过这个亏,后来全部改成手动位运算,一劳永逸。
3.5 实战经验总结
好了,四种数据类型都讲完了。最后分享几个我这些年积累下来的经验:
- 整数:永远显式处理字节序。写一个通用的字节序转换函数,所有整数解析都走它。
- 浮点数:先确认对方到底是不是真浮点。很多交易所为了精确,会用定点数替代。
- 字符串:长度前缀最安全。解析时一定要做边界检查,防止恶意数据包。
- 位域:手动位运算,别依赖编译器。写清楚宏定义或常量,方便后续维护。
嗯,这些就是二进制协议里最常见的数据类型。你想想看,搞懂了这些,解析任何交易所的协议都只是套模板的事。下一章咱们会讲更复杂的结构体与消息头设计,到时候这些基础类型都会用上。