从虚函数(virtual)的性质理解CTP期货量化交易API的设计原理

如果用一句话来概括CTP API的功能,那就是“将客户端的请求信息发往服务端,接收到服务端返回的信息时执行客户端开发者自定义的业务处理逻辑”。

CTP API的开发语言是C++,属于强类型语言,要求所有的类型在编译时都是确定的。就有两个问题需要解决:其一,API的功能实现逻辑需要封装到库文件里,因为暴露出来既不安全,对客户端开发者也不友好;其二,在编译好的库文件中,要能够调用进行客户端开发时才实现的业务处理逻辑

CTP API利用了虚函数沿着类的继承关系链向下穿透的性质,暴露两个基类CThostFtdcTraderApCThostFtdcTraderSpi的声明,实现了API的功能模块客户端自定义业务处理模块的隔离。

对于API的功能模块而言,客户端自定义业务处理模块的类型是CThostFtdcTraderSpi,使用这个类型可以调用到业务处理逻辑。相应的,对于客户端自定义业务处理模块而言,API的功能模块的类型是CThostFtdcTraderApi,使用这个类型可以调用API的功能。

接下来,我们就来了解一下,什么是虚函数,以及虚函数是如何向下穿透的。

声明前加上virtual关键字的函数就是虚函数虚函数的作用是,使用父类类型的指针调用函数时,能够向下穿透到子类中的实现逻辑。当然,只有是先将子类实例化,然后将子类指针转成父类类型的指针,父类类型指针调用虚函数才能穿透到子类的实现。如果是父类直接实例化,用这个父类实例的指针调用虚函数,执行的仍然是父类中的实现。

如果虚函数在声明时被赋值为0,就变成了纯虚函数,赋值为0是因为此处不需要实现。包含纯虚函数的类是纯虚类,通常被作为接口使用,所以也被称为接口类。因为纯虚类中声明的纯虚函数并没有被实现,所以纯虚类不能直接实例化,因此也被称为抽象类

必须由一个继承了纯虚类的子类,将所有纯虚函数都实现,才能正常使用。首先将实现了所有纯虚函数的子类实例化,再将子类的指针转成基类类型的指针,使用该基类类型的指针就可以调用子类中实现的功能

综上所示,基类通过(纯)虚函数实现了封装子类的目的。不同模块之间交互时,相互之间只需要使用基类类型的指针就可以调用需要的功能,不需要关心具体代码,实现了不同模块间代码的隔离。

1. CThostFtdcTraderApi

以CTP的交易API为例,CThostFtdcTraderApi就是被暴露出来的基类声明。库文件(dll/so)中存在一个实现了所有纯虚函数功能的子类,客户端开发者不知道这个子类的具体内容,但是可以通过一个基类类型CThostFtdcTraderApi的指针调用这个子类中实现的API功能。

初始化阶段,首先使用CreateFtdcTraderApi获得的一个CThostFtdcTraderApi类型的指针。

可以想象生成这个指针的过程:首先,将库文件中实现API功能模块的子类实例化,该子类继承基类CThostFtdcTraderApi并且实现了所有纯虚函数的功能;然后,将该子类指针转成基类类型CThostFtdcTraderApi的指针,该基类类型指针作为结果被返回。客户端可以使用这个返回值调用API功能模块。

基类CreateFtdcTraderApi声明的虚函数,确保基类类型的指针可以穿透到子类的功能代码并执行。例如,客户端认证请求函数在CThostFtdcTraderApi类中的声明为:

class CThostFtdcTraderApi
{
	///客户端认证请求
	virtual int ReqAuthenticate(CThostFtdcReqAuthenticateField *pReqAuthenticateField, int nRequestID) = 0;

};

这是一个纯虚函数。如下代码,在OnFrontConnected时,用m_pTraderApi执行ReqAuthenticate将客户端认证请求发往服务端。其中,m_pTraderApi是一个CThostFtdcTraderApi类型的指针,在初始化阶段通过CreateFtdcTraderApi获得。

void CTraderSpiImpl::OnFrontConnected()
{
	CThostFtdcReqAuthenticateField authenticateField = CThostFtdcReqAuthenticateField();
	memset(&authenticateField, 0, sizeof(CThostFtdcReqAuthenticateField));

	strcpy(authenticateField.BrokerID, "xxx");
	strcpy(authenticateField.UserID, "xxx");
	strcpy(authenticateField.AuthCode, "xxx");
	strcpy(authenticateField.AppID, "xxx");

	int nErrorID = m_pTraderApi->ReqAuthenticate(&authenticateField, 0);
	if (nErrorID != 0)
	{
        /// 发送失败
	}
}

2. CThostFtdcTraderSpi

CThostFtdcTraderSpi的情况类似。客户端开发者可以实现一个子类,例如命名为CTraderSpiImpl,对涉及到的业务响应函数进行重载,实现具体的业务处理逻辑。

子类CTraderSpiImpl实例化之后,被转成基类类型CThostFtdcTraderSpi的指针,并注册RegisterSpi到API功能模块的实例中。

从实现的先后顺序来看,API工作线程在先,客户端业务响应处理逻辑在后。正是基类CThostFtdcTraderSpi声明时使用了虚函数,先实现的API工作线程中才能调用后实现的客户端业务处理逻辑。

以下代码演示了SPI如何被注册到API中:

CThostFtdcTraderApi* m_pTraderApi;
m_pTraderApi = CThostFtdcTraderApi::CreateFtdcTraderApi(strFlowPath.c_str());

CTraderSpiImpl* pTraderSpi = new CTraderSpiImpl();
m_pTraderApi->RegisterSpi(pTraderSpi);

API功能模块对客户端业务处理模块调用时,使用的是基类类型CThostFtdcTraderSpi,也就是被RegisterSpi注册到API中的指针。

在收到信息时,API工作线程使用这个指针执行相关的SPI函数。例如,前面给出的OnFrontConnected的定义。当收到OnFrontConnected信息时,API工作线程执行客户端定义的功能,例子中执行的就是发出认证请求。

CThostFtdcTraderSpi中定义的响应函数不是纯虚函数,有默认的实现,空函数。所以,CThostFtdcTraderSpi是可以被实例化的,但是这样做没有意义,因为默认不执行任何操作。子类不需要将所有的响应函数都重新实现一遍,只需要实现和业务相关的响应就可以了。不相关的业务响应时,调用默认的空函数。

CThostFtdcTraderSpi中声明的函数都是以On开头,表明都是回调函数。从命名规则上,由可以细分为以下几类:

  • OnRspQry/OnRspQuery,响应查询的回调函数,与一个查询请求ReqQry/ReqQuery对应。只有发起请求的会话连接能收到。

  • OnRsp,响应非查询类业务的回调函数,与一个非查询类业务请求Req对应。也是只有发起请求的会话连接能收到。

  • OnRtn/OnErrRtn,收到服务端主动推送的信息时的回调,也被称为通知。所有的会话连接都能收到。

在不同的场景下,我们可能会提到回调响应通知推送触发等词,表达的都是API工作线程收到信息时执行由客户端定义的业务处理逻辑。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值