4、FIX协议深度解析:FIX协议结构、会话层与应用层、FIX引擎实现要点

说到FIX协议,很多做交易的朋友第一反应就是「那堆乱七八糟的标签号」。嗯,确实,FIX报文看起来就像一串数字和分隔符的堆砌。但说白了,FIX能成为全球金融电子交易的事实标准,靠的就是它那套精巧的分层设计。

我个人习惯把FIX协议拆成三块来看:物理结构(报文长什么样)、会话层(怎么建立和维持连接)、应用层(具体交易业务怎么表达)。今天咱们就一层层剥开它。

FIX协议三层架构 应用层 (Application Layer) 订单、成交、行情、风险控制等业务消息 会话层 (Session Layer) 登录、心跳、序列号管理、重传、登出 传输层 (Transport Layer) TCP/IP 或 SSL/TLS 加密传输

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字符。每个字段由标签号组成,中间用等号连接。

这里有几个关键标签你必须记住:

标签字段名说明
8BeginString协议版本,比如FIX.4.4
9BodyLength消息体长度(从35开始到10之前)
35MsgType消息类型,A=登录,D=新订单,8=成交
49SenderCompID发送方ID
56TargetCompID接收方ID
34MsgSeqNum消息序列号,会话层核心
52SendingTime发送时间
10CheckSum校验和,保证完整性

核心要点: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消息名用途
DNewOrderSingle下单
GOrderCancelReplaceRequest改单
FOrderCancelRequest撤单
8ExecutionReport成交回报
AETradeCaptureReport成交明细
WMarketDataSnapshotFullRefresh行情快照
XMarketDataIncrementalRefresh行情增量

下单消息(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