4、FIX协议深度解析:FIX协议结构、会话层与应用层、FIX引擎实现要点
说到FIX协议,很多做交易的朋友第一反应就是「那堆乱七八糟的标签号」。嗯,确实,FIX报文看起来就像一串数字和分隔符的堆砌。但说白了,FIX能成为全球金融电子交易的事实标准,靠的就是它那套精巧的分层设计。
我个人习惯把FIX协议拆成三块来看:物理结构(报文长什么样)、会话层(怎么建立和维持连接)、应用层(具体交易业务怎么表达)。今天咱们就一层层剥开它。
4.1 FIX报文结构:标签=值,就这么简单
FIX报文的核心就一句话:标签=值,用SOH分隔。SOH是ASCII码0x01,你看不到它,但它就是每个字段的分界符。
举个例子,一个最简单的登录消息长这样:
8=FIX.4.4|9=69|35=A|49=CLIENT1|56=BROKER1|34=1|52=20240501-12:00:00.000|98=0|108=30|10=123|
注意,上面的「|」是我为了展示方便写的,实际线上是SOH字符。每个字段由标签号和值组成,中间用等号连接。
这里有几个关键标签你必须记住:
| 标签 | 字段名 | 说明 |
|---|---|---|
| 8 | BeginString | 协议版本,比如FIX.4.4 |
| 9 | BodyLength | 消息体长度(从35开始到10之前) |
| 35 | MsgType | 消息类型,A=登录,D=新订单,8=成交 |
| 49 | SenderCompID | 发送方ID |
| 56 | TargetCompID | 接收方ID |
| 34 | MsgSeqNum | 消息序列号,会话层核心 |
| 52 | SendingTime | 发送时间 |
| 10 | CheckSum | 校验和,保证完整性 |
核心要点:FIX报文是自描述的。你不需要预定义结构,只要知道标签号,就能解析出含义。这也是FIX能灵活扩展的原因——自定义标签从5000开始用就行。
4.2 会话层:连接的生命线
会话层管的是「怎么安全可靠地交换消息」。我当年刚接触FIX时,觉得会话层就是登录登出,后来被生产环境教育了——序列号管理和重传机制才是会话层的灵魂。
4.2.1 登录与登出
登录消息(35=A)必须包含:
- 98=0:心跳间隔(单位秒)
- 108=30:心跳间隔(单位秒,与98重复但必须一致)
- 141=Y:是否重置序列号(可选)
登出消息(35=5)可以带一个文本字段58,说明原因。我曾经遇到过一个坑:某交易所要求登出时必须带58字段,否则不处理。嗯,文档里没写,踩了才知道。
4.2.2 序列号管理
每个会话双方各自维护一对序列号:发送序列号和接收序列号。规则很简单:
- 每发一条消息,发送序列号+1
- 收到消息后,检查接收序列号是否连续
- 如果发现跳号,说明丢消息了,需要发起重传
避坑指南:我曾经在生产环境遇到过一个诡异问题——序列号突然跳了1000多。排查了半天,发现是网关重启后序列号没持久化,从1重新开始发了。对方收到后以为丢包了,疯狂发重传请求。最后两台机器互相发重传,把带宽打满了。
解决方案:序列号必须持久化到磁盘,每次发送前先写盘再发消息。虽然慢一点,但安全。
4.2.3 心跳与测试请求
心跳(35=0)是保活机制。如果超过心跳间隔没收到任何消息,就发一个测试请求(35=1)。对方收到测试请求后,必须立即回复一个心跳。
我建议把心跳间隔设成30秒,别设太长。有一次我把间隔设成120秒,结果网络断了4分钟才发现,错过了好几笔交易。
4.2.4 重传机制
当发现序列号不连续时,发送方会发一个重传请求(35=2),指定要重传的序列号范围。接收方找到对应的消息,重新发送一遍。
这里有个性能陷阱:消息必须缓存起来,直到收到确认。缓存多久?我一般设成24小时,或者直到对方明确说「不用了」。缓存用环形缓冲区实现,别用链表,否则内存碎片会让你头疼。
4.3 应用层:业务逻辑的战场
应用层就是各种业务消息的集合。FIX定义了上百种消息类型,但常用的也就十几种。我列几个最核心的:
| MsgType | 消息名 | 用途 |
|---|---|---|
| D | NewOrderSingle | 下单 |
| G | OrderCancelReplaceRequest | 改单 |
| F | OrderCancelRequest | 撤单 |
| 8 | ExecutionReport | 成交回报 |
| AE | TradeCaptureReport | 成交明细 |
| W | MarketDataSnapshotFullRefresh | 行情快照 |
| X | MarketDataIncrementalRefresh | 行情增量 |
以下单消息(35=D)为例,核心字段包括:
- 11=ClOrdID:客户端订单ID,全局唯一
- 55=Symbol:合约代码
- 54=Side:买卖方向(1=买,2=卖)
- 38=OrderQty:数量
- 44=Price:价格(限价单必填)
- 40=OrdType:订单类型(1=市价,2=限价)
- 59=TimeInForce:有效期(0=当日有效,1=立即成交否则撤销)
实战技巧:ClOrdID一定要保证唯一性。我见过有人用时间戳+随机数,结果在高并发下重复了。建议用UUID或者雪花算法生成。另外,ClOrdID最好带上业务含义,比如「策略ID+日期+序列号」,方便排查问题。
4.4 FIX引擎实现要点
写一个FIX引擎,说白了就是实现上面说的会话层逻辑。我总结几个关键点:
4.4.1 解析器设计
FIX解析器要快。别用正则表达式,那玩意儿太慢了。我一般用状态机,逐字节解析:
- 状态1:读标签号
- 状态2:读等号
- 状态3:读值
- 状态4:读SOH分隔符
解析时直接映射到预分配的字段结构体,避免动态内存分配。我在项目中用内存池管理消息对象,解析完直接复用,GC压力小很多。
4.4.2 序列号持久化
前面说了,序列号必须持久化。我建议用内存映射文件(mmap),性能好,崩溃恢复也快。每次发送前先写mmap,再发消息。如果mmap写失败,说明磁盘有问题,直接断开连接。
4.4.3 消息缓存与重传
缓存用环形缓冲区,每个槽位存一条消息。缓冲区大小根据业务量来定,我一般设成10000条。如果缓冲区满了,就把最旧的消息覆盖掉——但这种情况说明对方处理太慢,应该报警。
重传时要注意:不要一次性把所有缓存消息全发出去。我曾经这么干过,结果把网络打爆了。应该分批发送,每批100条,中间间隔几毫秒。
4.4.4 心跳超时处理
心跳超时是常见问题。我一般设两个阈值:
- 软超时:超过心跳间隔1.5倍没收到消息,发测试请求
- 硬超时:超过心跳间隔3倍没收到任何响应,断开连接
断开后要自动重连,重连间隔从1秒开始,指数退避到30秒。别一直猛连,会把对方网关打挂。
总结一下:FIX协议看着复杂,其实核心就三块——报文结构、会话管理、业务消息。实现引擎时,序列号管理和重传机制是重中之重。很多坑都出在这两个地方。我建议你先把会话层跑通,再往上加业务逻辑。别一上来就搞全功能,容易翻车。
好了,FIX协议的核心内容就这些。下一章咱们聊聊怎么用C++写一个高性能的FIX引擎,包括零拷贝解析、无锁队列这些实战技巧。到时候我会把代码贴出来,咱们一行一行过。
公众号:蓝海资料掘金营,微信deep3321