4、交易所协议实战:FIX协议解析、WebSocket行情协议解析、基于状态机的TCP二进制协议解析
好,咱们终于到了最硬核的部分。前面几章讲了状态机的理论、设计模式、还有协议建模的方法论。这一章,咱们直接上战场——交易所的协议解析。
我个人习惯把交易所协议分成三类:文本型(FIX)、流式推送型(WebSocket行情)、二进制紧凑型(TCP私有协议)。这三类,几乎覆盖了市面上90%的交易所接口。你想想看,不管是做股票、期货、数字货币,底层无非就是这三板斧。
4.1 FIX协议解析:文本协议的“老大哥”
FIX协议,全称Financial Information eXchange。说实话,这玩意儿年纪比我还大(1992年就有了)。但你别看它老,华尔街那帮老牌券商、交易所,到现在还在用。为什么?稳定、成熟、生态好。
FIX协议的核心就一句话:键值对 + 分隔符。每个字段用“=”,字段之间用SOH(0x01)分隔。比如一条简单的订单消息:
8=FIX.4.2|9=78|35=D|49=CLIENT1|56=BROKER1|34=1|52=20240101-00:00:00|11=ORD001|55=AAPL|54=1|38=100|40=2|44=150.00|10=234|
(这里我用“|”代替了SOH字符,方便阅读)
你看,8=FIX.4.2表示协议版本,35=D表示消息类型是“新订单”,55=AAPL是股票代码,44=150.00是价格。非常直观。
但是!坑就藏在细节里。我曾经在对接某家期货公司时,发现他们的FIX网关对字段顺序有严格要求。标准FIX协议说字段可以乱序,但他们的实现就是按固定顺序解析。结果我这边发出去的订单,他们那边解析出来的价格和数量全对不上。排查了整整一个下午,最后发现是38(数量)和44(价格)的顺序反了。
用状态机来解析FIX协议,其实很简单。我一般设计这么几个状态:
- INIT:等待消息开始(检测到“8=”开头)
- HEADER:解析消息头(版本、消息类型、发送方等)
- BODY:解析消息体(根据消息类型决定字段集合)
- CHECKSUM:等待并验证校验和
- COMPLETE:消息解析完成
每个状态只做一件事,状态转移由SOH字符触发。这样设计的好处是,即使遇到半包(TCP粘包),状态机也能停在当前状态,等下一个数据包来了继续解析。
4.2 WebSocket行情协议解析:流式数据的“快车道”
WebSocket行情,说白了就是交易所把实时数据“推”给你,而不是你一遍遍去“拉”。这在量化交易里太重要了——你想想看,如果每秒钟要拉1000次行情,HTTP的开销就够你喝一壶的。
WebSocket协议本身是二进制帧格式,但交易所通常会在上面跑JSON或者自定义的二进制协议。我见过最多的,是JSON over WebSocket。比如某数字货币交易所的深度行情:
{
"type": "depthUpdate",
"symbol": "BTCUSDT",
"bids": [
[45000.00, 1.5],
[44999.50, 2.0]
],
"asks": [
[45001.00, 1.2],
[45001.50, 3.0]
]
}
JSON解析本身不复杂,但问题在于性能。我做过测试,用Python标准库的json.loads解析一条深度行情,大约需要50微秒。听起来很快对吧?但如果每秒来1000条,那就是50毫秒,CPU全花在解析上了。
我的优化方案是:用状态机做流式JSON解析。不等到完整JSON再解析,而是边接收边解析。比如检测到"bids"字段后,直接进入“解析数组”状态,每来一个价格-数量对,就更新本地的OrderBook。这样能省掉JSON的完整构建和反序列化开销。
WebSocket还有一个坑:心跳和重连。交易所通常每30秒发一个ping帧,你需要回pong。如果网络抖动导致心跳超时,交易所会断开连接。我建议在状态机里单独设计一个HEARTBEAT状态,专门处理ping/pong逻辑。如果连续3次心跳没收到,自动触发重连。
4.3 基于状态机的TCP二进制协议解析:最硬核的部分
终于到了我最喜欢的部分——二进制协议。为什么喜欢?因为快、省、可控。很多自营交易系统、高频交易平台,用的都是二进制协议。比如CTP(中国期货市场)、FIX的二进制变种FAST、还有各种交易所的私有协议。
二进制协议的结构通常是这样的:
+----------------+----------------+----------------+----------------+
| 消息头(8字节) | 消息体(变长) | 校验和(2字节) |
+----------------+----------------+----------------+----------------+
| 长度(2字节) | 类型(2字节) | 序列号(4字节) | 字段1 | 字段2 | ... | CRC16 |
+----------------+----------------+----------------+----------------+----------------+
你看,消息头固定8字节,包含长度、类型、序列号。消息体是变长的,根据长度字段决定。最后是2字节的CRC16校验。
用状态机来解析这种协议,我一般设计成五状态模型:
| 状态 | 描述 | 触发条件 |
|---|---|---|
| WAIT_HEADER | 等待消息头 | 收到至少8字节数据 |
| PARSE_HEADER | 解析消息头 | 读取长度、类型、序列号 |
| WAIT_BODY | 等待消息体 | 根据长度字段,等待足够字节 |
| PARSE_BODY | 解析消息体 | 按协议格式解析字段 |
| VERIFY_CHECKSUM | 校验CRC | 计算CRC并比对 |
这里有个关键点:WAIT_BODY状态必须能处理粘包。比如你期望收到100字节的消息体,但TCP只来了60字节。这时候状态机应该停在WAIT_BODY,记录已收到的字节数,等下一个数据包来了继续收。我见过太多人在这里踩坑——直接假设一次recv就能收完整个消息。
下面是我用Python实现的一个简化版二进制协议解析状态机:
class BinaryProtocolParser:
def __init__(self):
self.state = 'WAIT_HEADER'
self.buffer = b''
self.msg_length = 0
self.msg_type = 0
self.seq_num = 0
def feed(self, data):
self.buffer += data
while True:
if self.state == 'WAIT_HEADER':
if len(self.buffer) >= 8:
# 解析消息头
self.msg_length = int.from_bytes(self.buffer[0:2], 'big')
self.msg_type = int.from_bytes(self.buffer[2:4], 'big')
self.seq_num = int.from_bytes(self.buffer[4:8], 'big')
self.buffer = self.buffer[8:]
self.state = 'WAIT_BODY'
else:
break # 数据不够,等待更多
elif self.state == 'WAIT_BODY':
if len(self.buffer) >= self.msg_length:
body = self.buffer[:self.msg_length]
self.buffer = self.buffer[self.msg_length:]
self.state = 'VERIFY_CHECKSUM'
# 这里可以解析消息体
else:
break
elif self.state == 'VERIFY_CHECKSUM':
if len(self.buffer) >= 2:
received_crc = int.from_bytes(self.buffer[0:2], 'big')
# 计算CRC并比对
self.buffer = self.buffer[2:]
self.state = 'WAIT_HEADER'
# 返回解析完成的消息
else:
break
这段代码虽然简单,但体现了状态机的精髓:每个状态只关心自己需要的数据,数据不够就等,够了就处理并转移。
最后,我画了一张图,把三种协议的解析流程放在一起对比:
从这张图你能看出来,虽然三种协议长得不一样,但状态机的骨架是相同的:等待数据 → 解析数据 → 校验数据 → 完成。区别只在于每个阶段的具体实现。
嗯,这一章的内容就到这里。FIX协议的关键是字段顺序和校验和,WebSocket行情的关键是流式解析和心跳,TCP二进制协议的关键是粘包处理和CRC校验。把这些吃透了,市面上99%的交易所协议你都能搞定。