3、常见数据类型:整数类型(uint8/16/32/64)、浮点数类型、字符串与变长字段、位域与标志位

好,咱们直接切入正题。做二进制协议解析,说白了就是跟各种数据类型打交道。你想想看,交易所服务器发过来的数据,本质上就是一串字节流。怎么把这串字节流还原成有意义的行情数据、订单状态?关键就在于搞懂这些基础数据类型。

我个人习惯,把二进制协议里的数据类型分成四大类:整数、浮点数、字符串、位域。今天咱们一个一个过,每个我都会结合实战经验来讲。

3.1 整数类型:uint8/16/32/64

整数是二进制协议里最常用的类型。没有之一。交易所的订单号、价格、数量、时间戳,绝大多数都是整数。

先看一张图,帮你快速建立直观印象:

整数类型内存布局 uint8 1字节 uint16 2字节 uint32 4字节 uint64 8字节 字节数

这张图很直观: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个独立的标志位:

位域示例:1字节 = 8个标志位 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 订单类型 买卖方向 状态标志 保留位 保留位

你看,一个字节就能表示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 实战经验总结

好了,四种数据类型都讲完了。最后分享几个我这些年积累下来的经验:

  • 整数:永远显式处理字节序。写一个通用的字节序转换函数,所有整数解析都走它。
  • 浮点数:先确认对方到底是不是真浮点。很多交易所为了精确,会用定点数替代。
  • 字符串:长度前缀最安全。解析时一定要做边界检查,防止恶意数据包。
  • 位域:手动位运算,别依赖编译器。写清楚宏定义或常量,方便后续维护。

嗯,这些就是二进制协议里最常见的数据类型。你想想看,搞懂了这些,解析任何交易所的协议都只是套模板的事。下一章咱们会讲更复杂的结构体与消息头设计,到时候这些基础类型都会用上。


蓝海数据掘金营,专注资料整理